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:
Denis Blanchette
2023-02-06 20:50:15 +11:00
committed by GitHub
co-authored by Malik Ammar Akbar Enrico Minack
parent 7cf3dfc18e
commit 5e27c10a31
30 changed files with 817 additions and 304 deletions
+49 -1
View File
@@ -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