mirror of
https://github.com/status-im/PyGithub.git
synced 2026-09-02 11:51:10 +00:00
Support full GitHub app authentication (#1986)
* Support full GitHub app authentication Refactor GithubIntegration class and add test case for app authentication Add permissions and repository properties in InstallationAuthorization Set JWT_EXPIRY=60 by default in GithubIntegration constructor * Modify existing testcases for GithubIntegration as per the framework and add missing tests * Provide installation ID for creating the access token instead of getting the first installation * Add optional permissions support for installation access token * Add lock around app authentication * Keep compatibility for importing GithubIntegration from MainClass * Group app authentication parameters in a class Co-authored-by: Malik Ammar Akbar <malikammar.akbar@pfizer.com> Co-authored-by: Enrico Minack <github@enrico.minack.dev>
This commit is contained in:
co-authored by
Malik Ammar Akbar
Enrico Minack
parent
7cf3dfc18e
commit
5e27c10a31
+14
-113
@@ -49,10 +49,7 @@
|
||||
|
||||
import datetime
|
||||
import pickle
|
||||
import time
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
import github.ApplicationOAuth
|
||||
@@ -68,24 +65,13 @@ from . import (
|
||||
AuthenticatedUser,
|
||||
Consts,
|
||||
GithubApp,
|
||||
GithubException,
|
||||
GitignoreTemplate,
|
||||
HookDescription,
|
||||
Installation,
|
||||
InstallationAuthorization,
|
||||
RateLimit,
|
||||
Repository,
|
||||
)
|
||||
from .Requester import Requester
|
||||
|
||||
DEFAULT_BASE_URL = "https://api.github.com"
|
||||
DEFAULT_STATUS_URL = "https://status.github.com"
|
||||
# As of 2018-05-17, Github imposes a 10s limit for completion of API requests.
|
||||
# Thus, the timeout should be slightly > 10s to account for network/front-end
|
||||
# latency.
|
||||
DEFAULT_TIMEOUT = 15
|
||||
DEFAULT_PER_PAGE = 30
|
||||
|
||||
|
||||
class Github:
|
||||
"""
|
||||
@@ -97,10 +83,11 @@ class Github:
|
||||
login_or_token=None,
|
||||
password=None,
|
||||
jwt=None,
|
||||
base_url=DEFAULT_BASE_URL,
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
app_auth=None,
|
||||
base_url=Consts.DEFAULT_BASE_URL,
|
||||
timeout=Consts.DEFAULT_TIMEOUT,
|
||||
user_agent="PyGithub/Python",
|
||||
per_page=DEFAULT_PER_PAGE,
|
||||
per_page=Consts.DEFAULT_PER_PAGE,
|
||||
verify=True,
|
||||
retry=None,
|
||||
pool_size=None,
|
||||
@@ -108,6 +95,8 @@ class Github:
|
||||
"""
|
||||
:param login_or_token: string
|
||||
:param password: string
|
||||
:param jwt: string
|
||||
:param app_auth: github.AppAuthentication
|
||||
:param base_url: string
|
||||
:param timeout: integer
|
||||
:param user_agent: string
|
||||
@@ -125,14 +114,16 @@ class Github:
|
||||
assert user_agent is None or isinstance(user_agent, str), user_agent
|
||||
assert (
|
||||
retry is None
|
||||
or isinstance(retry, (int))
|
||||
or isinstance(retry, (urllib3.util.Retry))
|
||||
)
|
||||
assert pool_size is None or isinstance(pool_size, (int)), pool_size
|
||||
or isinstance(retry, int)
|
||||
or isinstance(retry, urllib3.util.Retry)
|
||||
), retry
|
||||
assert pool_size is None or isinstance(pool_size, int), pool_size
|
||||
|
||||
self.__requester = Requester(
|
||||
login_or_token,
|
||||
password,
|
||||
jwt,
|
||||
app_auth,
|
||||
base_url,
|
||||
timeout,
|
||||
user_agent,
|
||||
@@ -786,95 +777,5 @@ class Github:
|
||||
return GithubApp.GithubApp(self.__requester, headers, data, completed=True)
|
||||
|
||||
|
||||
class GithubIntegration:
|
||||
"""
|
||||
Main class to obtain tokens for a GitHub integration.
|
||||
"""
|
||||
|
||||
def __init__(self, integration_id, private_key, base_url=DEFAULT_BASE_URL):
|
||||
"""
|
||||
:param base_url: string
|
||||
:param integration_id: int
|
||||
:param private_key: string
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.integration_id = integration_id
|
||||
self.private_key = private_key
|
||||
assert isinstance(base_url, str), base_url
|
||||
|
||||
def create_jwt(self, expiration=60):
|
||||
"""
|
||||
Creates a signed JWT, valid for 60 seconds by default.
|
||||
The expiration can be extended beyond this, to a maximum of 600 seconds.
|
||||
|
||||
:param expiration: int
|
||||
:return string:
|
||||
"""
|
||||
now = int(time.time())
|
||||
payload = {"iat": now, "exp": now + expiration, "iss": self.integration_id}
|
||||
encrypted = jwt.encode(payload, key=self.private_key, algorithm="RS256")
|
||||
|
||||
if isinstance(encrypted, bytes):
|
||||
encrypted = encrypted.decode("utf-8")
|
||||
|
||||
return encrypted
|
||||
|
||||
def get_access_token(self, installation_id, user_id=None):
|
||||
"""
|
||||
Get an access token for the given installation id.
|
||||
POSTs https://api.github.com/app/installations/<installation_id>/access_tokens
|
||||
:param user_id: int
|
||||
:param installation_id: int
|
||||
:return: :class:`github.InstallationAuthorization.InstallationAuthorization`
|
||||
"""
|
||||
body = {}
|
||||
if user_id:
|
||||
body = {"user_id": user_id}
|
||||
response = requests.post(
|
||||
f"{self.base_url}/app/installations/{installation_id}/access_tokens",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.create_jwt()}",
|
||||
"Accept": Consts.mediaTypeIntegrationPreview,
|
||||
"User-Agent": "PyGithub/Python",
|
||||
},
|
||||
json=body,
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
return InstallationAuthorization.InstallationAuthorization(
|
||||
requester=None, # not required, this is a NonCompletableGithubObject
|
||||
headers={}, # not required, this is a NonCompletableGithubObject
|
||||
attributes=response.json(),
|
||||
completed=True,
|
||||
)
|
||||
elif response.status_code == 403:
|
||||
raise GithubException.BadCredentialsException(
|
||||
status=response.status_code, data=response.text
|
||||
)
|
||||
elif response.status_code == 404:
|
||||
raise GithubException.UnknownObjectException(
|
||||
status=response.status_code, data=response.text
|
||||
)
|
||||
raise GithubException.GithubException(
|
||||
status=response.status_code, data=response.text
|
||||
)
|
||||
|
||||
def get_installation(self, owner, repo):
|
||||
"""
|
||||
:calls: `GET /repos/{owner}/{repo}/installation <https://docs.github.com/en/rest/reference/apps#get-a-repository-installation-for-the-authenticated-app>`_
|
||||
:param owner: str
|
||||
:param repo: str
|
||||
:rtype: :class:`github.Installation.Installation`
|
||||
"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.create_jwt()}",
|
||||
"Accept": Consts.mediaTypeIntegrationPreview,
|
||||
"User-Agent": "PyGithub/Python",
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
f"{self.base_url}/repos/{owner}/{repo}/installation",
|
||||
headers=headers,
|
||||
)
|
||||
response_dict = response.json()
|
||||
return Installation.Installation(None, headers, response_dict, True)
|
||||
# Retrocompatibility
|
||||
GithubIntegration = github.GithubIntegration
|
||||
|
||||
Reference in New Issue
Block a user