mirror of
https://github.com/status-im/PyGithub.git
synced 2026-08-31 10:51:14 +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
+49
-1
@@ -51,6 +51,7 @@
|
||||
################################################################################
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
@@ -59,10 +60,14 @@ import re
|
||||
import time
|
||||
import urllib
|
||||
from io import IOBase
|
||||
from multiprocessing import RLock
|
||||
|
||||
import requests
|
||||
|
||||
from . import Consts, GithubException
|
||||
from . import Consts, GithubException, GithubIntegration
|
||||
|
||||
# For App authentication, time remaining before token expiration to request a new one
|
||||
ACCESS_TOKEN_REFRESH_THRESHOLD_SECONDS = 20
|
||||
|
||||
|
||||
class RequestsResponse:
|
||||
@@ -294,6 +299,7 @@ class Requester:
|
||||
login_or_token,
|
||||
password,
|
||||
jwt,
|
||||
app_auth,
|
||||
base_url,
|
||||
timeout,
|
||||
user_agent,
|
||||
@@ -304,6 +310,11 @@ class Requester:
|
||||
):
|
||||
self._initializeDebugFeature()
|
||||
|
||||
self.__installation_authorization = None
|
||||
self.__app_auth = app_auth
|
||||
|
||||
self.__auth_lock = RLock()
|
||||
|
||||
if password is not None:
|
||||
login = login_or_token
|
||||
b64 = (
|
||||
@@ -317,6 +328,8 @@ class Requester:
|
||||
self.__authorizationHeader = f"token {token}"
|
||||
elif jwt is not None:
|
||||
self.__authorizationHeader = f"Bearer {jwt}"
|
||||
elif self.__app_auth is not None:
|
||||
self._refresh_token()
|
||||
else:
|
||||
self.__authorizationHeader = None
|
||||
|
||||
@@ -349,6 +362,40 @@ class Requester:
|
||||
self.__userAgent = user_agent
|
||||
self.__verify = verify
|
||||
|
||||
def _must_refresh_token(self) -> bool:
|
||||
"""Check if it is time to refresh the API token gotten from the GitHub app installation"""
|
||||
if not self.__installation_authorization:
|
||||
return False
|
||||
return (
|
||||
self.__installation_authorization.expires_at
|
||||
< datetime.datetime.utcnow()
|
||||
+ datetime.timedelta(seconds=ACCESS_TOKEN_REFRESH_THRESHOLD_SECONDS)
|
||||
)
|
||||
|
||||
def _get_installation_authorization(self):
|
||||
assert self.__app_auth is not None
|
||||
integration = GithubIntegration.GithubIntegration(
|
||||
self.__app_auth.app_id, self.__app_auth.private_key
|
||||
)
|
||||
return integration.get_access_token(
|
||||
self.__app_auth.installation_id,
|
||||
permissions=self.__app_auth.token_permissions,
|
||||
)
|
||||
|
||||
def _refresh_token_if_needed(self) -> None:
|
||||
"""Get a new access token from the GitHub app installation if the one we have is about to expire"""
|
||||
if not self.__installation_authorization:
|
||||
return
|
||||
with self.__auth_lock:
|
||||
if self._must_refresh_token():
|
||||
logging.debug("Refreshing access token")
|
||||
self._refresh_token()
|
||||
|
||||
def _refresh_token(self) -> None:
|
||||
"""In the context of a GitHub app, refresh the access token"""
|
||||
self.__installation_authorization = self._get_installation_authorization()
|
||||
self.__authorizationHeader = f"token {self.__installation_authorization.token}"
|
||||
|
||||
def requestJsonAndCheck(self, verb, url, parameters=None, headers=None, input=None):
|
||||
return self.__check(
|
||||
*self.requestJson(
|
||||
@@ -578,6 +625,7 @@ class Requester:
|
||||
return status, responseHeaders, output
|
||||
|
||||
def __authenticate(self, url, requestHeaders, parameters):
|
||||
self._refresh_token_if_needed()
|
||||
if self.__authorizationHeader is not None:
|
||||
requestHeaders["Authorization"] = self.__authorizationHeader
|
||||
|
||||
|
||||
Reference in New Issue
Block a user