8000 Merge pull request #889 from sigmavirus24/apps-support · pythonthings/github3.py@830b562 · GitHub
[go: up one dir, main page]

Skip to content

Commit 830b562

Browse files
authored
Merge pull request sigmavirus24#889 from sigmavirus24/apps-support
Add support for GitHub Apps
2 parents 5eff8c6 + 416ecc2 commit 830b562

File tree

14 files changed

+780
-10
lines changed

14 files changed

+780
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ tests/id_rsa
2323
.mypy_cache/
2424
.pytest_cache/
2525
t.py
26+
*.pem

docs/source/api-reference/apps.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
==================================
2+
App and Installation API Objects
3+
==================================
4+
5+
This section of the documentation covers the representations of various
6+
objects related to the `Apps API`_.
7+
8+
.. autoclass:: github3.apps.App
9+
:inherited-members:
10+
11+
.. autoclass:: github3.apps.Installation
12+
:inherited-members:
13+
14+
15+
.. ---
16+
.. links
17+
.. _Apps API:
18+
https://developer.github.com/v3/apps

docs/source/api-reference/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
:maxdepth: 3
77

88
api
9+
apps
910
auths
1011
events
1112
gists

docs/source/release-notes/1.2.0.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,38 @@ This is a small release with some enhancements.
66
Features Added
77
``````````````
88

9+
- Partial GitHub Apps support. We added the following:
10+
11+
- ``GitHub.login_as_app`` to login using JWT as an Application
12+
13+
- ``GitHub.login_as_app_installation`` to login using a token obtained from
14+
an App's JWT
15+
16+
- ``GitHub.app`` to retrieve an application by its "slug"
17+
18+
- ``GitHub.app_installation`` to retrieve a specific installation by its ID
19+
20+
- ``GitHub.app_installations`` to retrieve all of an App's installations
21+
22+
- ``GitHub.app_installation_for_organization`` to retrieve an organization's
23+
installation of an App
24+
25+
- ``GitHub.app_installation_for_repository`` to retrieve an installation for
26+
a specific repository
27+
28+
- ``GitHub.app_installation_for_user`` to retrieve an installation for a
29+
specific user
30+
31+
- ``GitHub.authenticated_app`` to retrieve the metadata for a specific App
32+
33+
- Not supported as of this release:
34+
35+
- `Installations API`_
36+
37+
- `List installations for user`_
38+
39+
- `User-to-server OAuth access token`_
40+
941
- Organization Invitations Preview API is now supported. This includes an
1042
additional ``Invitation`` object. This is the result of hard work by Hal
1143
Wine.
@@ -14,3 +46,14 @@ Features Added
1446
representation of labels returned by the API.
1547

1648
- The branch protections API is now completely represented in github3.py.
49+
50+
51+
.. links
52+
.. _Installations API:
53+
https://developer.github.com/v3/apps/installations/
54+
55+
.. _List installations for user:
56+
https://developer.github.com/v3/apps/#list-installations-for-user
57+
58+
.. _User-to-server OAuth access token:
59+
https://developer.github.com/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps/#identifying-users-on-your-site

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ requires-dist=
66
requests>=2.0
77
uritemplate>=3.0.0
88
python-dateutil>=2.6.0
9+
jwcrypto>=0.5.0
910
pyOpenSSL>=0.13; python_version<="2.7"
1011
ndg-httpsclient; python_version<="2.7"
1112
pyasn1; python_version<="2.7"

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
SNI_requirements = [
2222
'pyOpenSSL',
2323
'ndg-httpsclient',
24-
'pyasn1'
24+
'pyasn1',
2525
]
2626

2727
kwargs['tests_require'] = ['betamax>=0.8.0', 'pytest>2.3.5',
@@ -39,6 +39,7 @@
3939
"requests >= 2.18",
4040
"uritemplate >= 3.0.0",
4141
"python-dateutil >= 2.6.0",
42+
"jwcrypto >= 0.5.0",
4243
])
4344

4445
__version__ = ''

src/github3/apps.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"""Module containing helpers for dealing with GitHub Apps.
2+
3+
https://developer.github.com/apps/building-github-apps/
4+
"""
5+
import time
6+
7+
from jwcrypto import jwk
8+
from jwcrypto import jwt
9+
10+
from . import models
11+
from . import users
12+
13+
TEN_MINUTES_AS_SECONDS = 10 * 60
14+
DEFAULT_JWT_TOKEN_EXPIRATION = TEN_MINUTES_AS_SECONDS
15+
APP_PREVIEW_HEADERS = {
16+
'Accept': 'application/vnd.github.machine-man-preview+json',
17+
}
18+
19+
20+
class App(models.GitHubCore):
21+
"""An object representing a GitHub App.
22+
23+
.. versionadded:: 1.2.0
24+
25+
.. seealso::
26+
27+
`GitHub Apps`_
28+
Documentation for Apps on GitHub
29+
30+
`GitHub Apps API Documentation`_
31+
API documentation of what's available about an App.
32+
33+
This object has the following attributes:
34+
35+
.. attribute:: created_at
36+
37+
A :class:`~datetime.datetime` object representing the day and time
38+
the App was created.
39+
40+
.. attribute:: description
41+
42+
The description of the App provided by the owner.
43+
44+
.. attribute:: external_url
45+
46+
The URL provided for the App by the owner.
47+
48+
.. attribute:: html_url
49+
50+
The HTML URL provided for the App by the owner.
51+
52+
.. attribute:: id
53+
54+
The unique identifier for the App. This is useful in cases where you
55+
may want to authenticate either as an App or as a specific
56+
installation of an App.
57+
58+
.. attribute:: name
59+
60+
The display name of the App that the user sees.
61+
62+
.. attribute:: node_id
63+
64+
A base64-encoded blob returned by the GitHub API for who knows what
65+
reason.
66+
67+
.. attribute:: owner
68+
69+
A :class:`~github3.users.ShortUser` object representing the GitHub
70+
user who owns the App.
71+
72+
.. attribute:: updated_at
73+
74+
A :class:`~datetime.datetime` object representing the day and time
75+
the App was last updated.
76+
77+
.. _GitHub Apps:
78+
https://developer.github.com/apps/
79+
.. _GitHub Apps API Documentation:
80+
https://developer.github.com/v3/apps/
81+
"""
82+
83+
def _update_attributes(self, json):
84+
self.created_at = self._strptime(json['created_at'])
85+
self.description = json['description']
86+
self.external_url = json['external_url']
87+
self.html_url = json['html_url']
88+
self.id = json['id']
89+
self.name = json['name']
90+
self.node_id = json['node_id']
91+
self.owner = users.ShortUser(json['owner'], self)
92+
self.updated_at = self._strptime(json['updated_at'])
93+
94+
def _repr(self):
95+
return '<App ["{}" by {}]>'.format(self.name, str(self.owner))
96+
97+
98+
class Installation(models.GitHubCore):
99+
"""An installation of a GitHub App either on a User or Org.
100+
101+
.. versionadded:: 1.2.0
102+
103+
This has the following attributes:
104+
105+
.. attribute:: access_tokens_url
106+
.. attribute:: account
107+
.. attribute:: app_id
108+
.. attribute:: created_at
109+
.. attribute:: events
110+
.. attribute:: html_url
111+
.. attribute:: id
112+
.. attribute:: permissions
113+
.. attribute:: repositories_url
114+
.. attribute:: repository_selection
115+
.. attribute:: single_file_name
116+
.. attribute:: target_id
117+
.. attribute:: target_type
118+
.. attribute:: updated_at
119+
"""
120+
121+
def _update_attributes(self, json):
122+
self.access_tokens_url = json['access_tokens_url']
123+
self.account = json['account']
124+
self.app_id = json['app_id']
125+
self.created_at = self._strptime(json['created_at'])
126+
self.events = json['events']
127+
self.html_url = json['html_url']
128+
self.id = json['id']
129+
self.permissions = json['permissions']
130+
self.repositories_url = json['repositories_url']
131+
self.repository_selection = json['repository_selection']
132+
self.single_file_name = json['single_file_name']
133+
self.target_id = json['target_id']
134+
self.target_type = json['target_type']
135+
self.updated_at = self._strptime(json['updated_at'])
136+
137+
138+
def _load_private_key(pem_key_bytes):
139+
return jwk.JWK.from_pem(pem_key_bytes)
140+
141+
142+
def create_token(private_key_pem, app_id, expire_in=TEN_MINUTES_AS_SECONDS):
143+
"""Create an encrypted token for the specified App.
144+
145+
:param bytes private_key_pem:
146+
The bytes of the private key for this GitHub Application.
147+
:param int app_id:
148+
The integer identifier for this GitHub Application.
149+
:param int expire_in:
150+
The length in seconds for this token to be valid for.
151+
Default: 600 seconds (10 minutes)
152+
:returns:
153+
Serialized encrypted token.
154+
:rtype:
155+
text
156+
"""
157+
if not isinstance(private_key_pem, bytes):
158+
raise ValueError('"private_key_pem" parameter must be byte-string')
159+
key = _load_private_key(private_key_pem)
160+
now = int(time.time())
161+
token = jwt.JWT(
162+
header={
163+
'alg': 'RS256'
164+
},
165+
claims={
166+
'iat': now,
167+
'exp': now + expire_in,
168+
'iss': app_id,
169+
},
170+
algs=['RS256'],
171+
)
172+
token.make_signed_token(key)
173+
return token.serialize()
174+
175+
176+
def create_jwt_headers(private_key_pem, app_id,
177+
expire_in=DEFAULT_JWT_TOKEN_EXPIRATION):
178+
"""Create an encrypted token for the specified App.
179+
180+
:param bytes private_key_pem:
181+
The bytes of the private key for this GitHub Application.
182+
:param int app_id:
183+
The integer identifier for this GitHub Application.
184+
:param int expire_in:
185+
The length in seconds for this token to be valid for.
186+
Default: 600 seconds (10 minutes)
187+
:returns:
188+
Dictionary of headers for retrieving a token from a JWT.
189+
:rtype:
190+
dict
191+
"""
192+
jwt_token = create_token(private_key_pem, app_id, expire_in)
193+
headers = {'Authorization': 'Bearer {}'.format(jwt_token)}
194+
headers.update(APP_PREVIEW_HEADERS)
195+
return headers

src/github3/decorators.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,42 @@ def auth_wrapper(self, *args, **kwargs):
8282
return auth_wrapper
8383

8484

85+
def requires_app_bearer_auth(func):
86+
"""Require the use of application authentication.
87+
88+
.. versionadded:: 1.2.0
89+
"""
90+
@wraps(func)
91+
def auth_wrapper(self, *args, **kwargs):
92+
from . import session
93+
if isinstance(self.session.auth, session.AppBearerTokenAuth):
94+
return func(self, *args, **kwargs)
95+
else:
96+
from . import exceptions
97+
raise exceptions.MissingAppBearerAuthentication(
98+
"This method requires GitHub App authentication."
99+
)
100+
return auth_wrapper
101+
102+
103+
def requires_app_installation_auth(func):
104+
"""Require the use of App's installation authentication.
105+
106+
.. versionadded:: 1.2.0
107+
"""
108+
@wraps(func)
109+
def auth_wrapper(self, *args, **kwargs):
110+
from . import session
111+
if isinstance(self.session.auth, session.AppInstallationTokenAuth):
112+
return func(self, *args, **kwargs)
113+
else:
114+
from . import exceptions
115+
raise exceptions.MissingAppInstallationAuthentication(
116+
"This method requires GitHub App authentication."
117+
)
118+
return auth_wrapper
119+
120+
85121
def generate_fake_error_response(msg, status_code=401, encoding='utf-8'):
86122
"""Generate a fake Response from requests."""
87123
r = Response()

0 commit comments

Comments
 (0)
0