Skip to content

Commit cc4f695

Browse files
committed
add: init commit
1 parent 040f25d commit cc4f695

File tree

3 files changed

+445
-0
lines changed

3 files changed

+445
-0
lines changed

githubapp/adapter.py

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
import json
2+
from abc import ABCMeta, abstractmethod
3+
from typing import List, Optional, Union
4+
import jwt
5+
import aiohttp
6+
import datetime
7+
import requests
8+
import re
9+
import urllib.parse
10+
from cryptography.hazmat.backends import default_backend
11+
from cryptography.hazmat.primitives import serialization
12+
import pprint
13+
14+
from .errors import (BadRequest, Forbidden, HTTPException, InternalServerError,
15+
NotFound, PayloadTooLarge, QuotaExceeded,
16+
ServiceUnavailable, TooManyRequests, URITooLong)
17+
18+
__all__ = ['Authentication', 'Auth']
19+
20+
21+
class Authentication(metaclass=ABCMeta):
22+
def __init__(self, app_id: int, installation_id: int, client_secret, *, iat: int = 30, exp: int = 30):
23+
self.installation_id = installation_id
24+
self.endpoint = 'https://api.github.com/installations/{}/access_tokens'.format(
25+
self.installation_id)
26+
self.algorithm = "RS256"
27+
self.app_id = app_id
28+
self.client_secret = client_secret
29+
self.iat = iat
30+
self.exp = exp
31+
self.now = datetime.datetime.now(datetime.timezone.utc)
32+
33+
@abstractmethod
34+
def gen_jwt(self) -> bytes:
35+
raise NotImplementedError()
36+
37+
@abstractmethod
38+
def gen_pubkey(self) -> bytes:
39+
raise NotImplementedError()
40+
41+
@abstractmethod
42+
def is_authorization(self, _jwt: str, client_public: str) -> bool:
43+
raise NotImplementedError()
44+
45+
@abstractmethod
46+
def get_access_token_response(self, jwt: str, **kwargs) -> Optional[Union[list, dict]]:
47+
raise NotImplementedError()
48+
49+
@abstractmethod
50+
def get_access_token(self, *, access_token_response: str) -> str:
51+
raise NotImplementedError()
52+
53+
@abstractmethod
54+
def get_usage(self) -> dict:
55+
raise NotImplementedError()
56+
57+
def _check_status(self, status_code, response, data) -> Union[dict, list]:
58+
if 200 <= status_code < 300:
59+
return data
60+
message = data.get('message', '') if data else ''
61+
if status_code == 400:
62+
raise BadRequest(response, message)
63+
elif status_code == 403:
64+
raise Forbidden(response, message)
65+
elif status_code == 404:
66+
raise NotFound(response, message)
67+
elif status_code == 413:
68+
raise PayloadTooLarge(response, message)
69+
elif status_code == 414:
70+
raise URITooLong(response, message)
71+
elif status_code == 429:
72+
raise TooManyRequests(response, message)
73+
elif status_code == 456:
74+
raise QuotaExceeded(response, message)
75+
elif status_code == 503:
76+
raise ServiceUnavailable(response, message)
77+
elif 500 <= status_code < 600:
78+
raise InternalServerError(response, message)
79+
else:
80+
raise HTTPException(response, message)
81+
82+
83+
class Auth(Authentication):
84+
"""Researchmap authentication interface.
85+
86+
Parameters
87+
----------
88+
app_id: :class:`str`
89+
Client ID.
90+
client_secret: :class:`bytes`
91+
Client secret key.
92+
93+
Keyword Arguments
94+
-----------------
95+
iat: :class:`int`
96+
Issued at [sec].
97+
exp: :class:`int`
98+
Expire at [sec].
99+
trial: :class:`bool`
100+
Trial mode.
101+
"""
102+
103+
@property
104+
def is_trial(self) -> bool:
105+
"""Get trial mode.
106+
107+
Returns
108+
-------
109+
:class:`bool`
110+
Trial mode.
111+
"""
112+
return self.trial
113+
114+
@property
115+
def time_now(self) -> datetime.datetime:
116+
"""Get current time [aware].
117+
118+
Returns
119+
-------
120+
:class:`datetime.datetime`
121+
Current time of UTC.
122+
"""
123+
return self.now
124+
125+
@property
126+
def time_iat(self) -> datetime.datetime:
127+
"""Get issued at time [aware].
128+
129+
Returns
130+
-------
131+
:class:`datetime.datetime`
132+
Issued at time of UTC.
133+
"""
134+
return self.now - datetime.timedelta(seconds=self.iat)
135+
136+
@property
137+
def time_exp(self) -> datetime.datetime:
138+
"""Get expire at time [aware].
139+
140+
Returns
141+
-------
142+
:class:`datetime.datetime`
143+
Expire at time of UTC.
144+
"""
145+
return self.now + datetime.timedelta(seconds=self.exp)
146+
147+
@property
148+
def token(self) -> str:
149+
"""Get token.
150+
151+
Returns
152+
-------
153+
:class:`str`
154+
Token.
155+
156+
Raises
157+
------
158+
:exc:`InvalidToken`
159+
Invalid token.
160+
:class:`json.JSONDecodeError`
161+
JSON decode error.
162+
:class:`requests.exceptions.HTTPError`
163+
HTTP error.
164+
"""
165+
return self.get_access_token()
166+
167+
def gen_jwt(self, *, exp: int = None, iat: int = None) -> bytes:
168+
"""Generate JWT.
169+
170+
Keyword Arguments
171+
-----------------
172+
exp: :class:`int`
173+
Expire at [sec].
174+
iat: :class:`int`
175+
Issued at [sec].
176+
177+
Returns
178+
-------
179+
:class:`bytes`
180+
JWT.
181+
"""
182+
if exp is None:
183+
exp = self.exp
184+
if iat is None:
185+
iat = self.iat
186+
187+
payload = {
188+
"iss": self.app_id,
189+
"iat": self.now - datetime.timedelta(seconds=iat),
190+
"exp": self.now + datetime.timedelta(seconds=exp),
191+
}
192+
_jwt = jwt.encode(payload, self.client_secret,
193+
algorithm=self.algorithm)
194+
return _jwt
195+
196+
def gen_pubkey(self, *, client_secret: str = None) -> bytes:
197+
"""
198+
Generate public key.
199+
200+
Keyword Arguments
201+
-----------------
202+
client_secret: :class:`str`
203+
Client secret key.
204+
205+
Returns
206+
-------
207+
:class:`bytes`
208+
Client public key.
209+
"""
210+
if client_secret is None:
211+
client_secret = self.client_secret
212+
213+
privkey = serialization.load_pem_private_key(
214+
client_secret,
215+
password=None,
216+
backend=default_backend()
217+
)
218+
pubkey = privkey.public_key()
219+
client_public = pubkey.public_bytes(
220+
serialization.Encoding.PEM,
221+
serialization.PublicFormat.SubjectPublicKeyInfo
222+
)
223+
return client_public
224+
225+
def is_authorization(self, *, _jwt: str = None, client_public: str = None) -> bool:
226+
"""Check authorization.
227+
228+
Keyword Arguments
229+
-----------------
230+
_jwt: :class:`str`
231+
JWT.
232+
client_public: :class:`str`
233+
Client public key.
234+
235+
Returns
236+
-------
237+
:class:`bool`
238+
True if authorization.
239+
240+
Raises
241+
------
242+
:class:`jwt.InvalidTokenError`
243+
Invalid JWT.
244+
245+
"""
246+
if _jwt is None:
247+
_jwt = self.gen_jwt()
248+
if client_public is None:
249+
client_public = self.gen_pubkey()
250+
try:
251+
decoded_jwt = jwt.decode(_jwt, key=client_public,
252+
audience=self.endpoint, algorithms=self.algorithm)
253+
if decoded_jwt['iss'] == self.app_id:
254+
return True
255+
except:
256+
print("The signature of JWT cannot be verified.")
257+
return False
258+
259+
def get_access_token_response(self, *, _jwt: bytes = None, **kwargs) -> Optional[Union[list, dict]]:
260+
"""Get access token.
261+
262+
Keyword Arguments
263+
----------
264+
_jwt: :class:`bytes`
265+
JWT.
266+
267+
Returns
268+
-------
269+
Optional[Union[:class:`list`, :class:`dict`]]
270+
Access token.
271+
272+
Raises
273+
------
274+
:exc:`HTTPException`
275+
An unknown HTTP related error occurred, usually when it isn’t 200 or the known incorrect credentials passing status code.
276+
"""
277+
if _jwt is None:
278+
_jwt = self.gen_jwt()
279+
280+
headers = {
281+
'Accept': 'application/vnd.github.machine-man-preview+json',
282+
'Authorization': 'Bearer {}'.format(_jwt),
283+
}
284+
if self.is_authorization():
285+
req_access_token = requests.post(
286+
url=self.endpoint, headers=headers)
287+
try:
288+
data = req_access_token.json()
289+
except json.JSONDecodeError:
290+
print(req_access_token.content)
291+
return self._check_status(req_access_token.status_code, req_access_token, data)
292+
else:
293+
print("Access Token is not valid")
294+
295+
def get_access_token(self, *, access_token_response: Optional[Union[list, dict]] = None) -> str:
296+
"""Get access token.
297+
298+
Keyword Arguments
299+
----------
300+
access_token_response: :class: Optional[Union[:class:`list`, :class:`dict`]]
301+
Access token response.
302+
303+
Returns
304+
-------
305+
:class:`str`
306+
Access token.
307+
308+
Raises
309+
------
310+
:class:`TypeError`
311+
The type of the argument is not correct.
312+
:exc:`HTTPException`
313+
An unknown HTTP related error occurred, usually when it isn’t 200 or the known incorrect credentials passing status code.
314+
:exc:`InvalidToken`
315+
Invalid token.
316+
"""
317+
if access_token_response is None:
318+
access_token_response = self.get_access_token_response()
319+
return access_token_response['access_token']
320+
321+
def get_usage(self) -> None:
322+
return None

0 commit comments

Comments
 (0)