From 5e27c10a3140c3b9bbf71a0b71c96e71e1e3496c Mon Sep 17 00:00:00 2001 From: Denis Blanchette Date: Mon, 6 Feb 2023 04:50:15 -0500 Subject: [PATCH] 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 Co-authored-by: Enrico Minack --- github/AppAuthentication.py | 41 +++ github/AppAuthentication.pyi | 10 + github/Consts.py | 17 + github/GithubIntegration.py | 199 +++++++++++ github/GithubIntegration.pyi | 34 ++ github/InstallationAuthorization.py | 22 ++ github/InstallationAuthorization.pyi | 4 + github/MainClass.py | 127 +------ github/MainClass.pyi | 21 +- github/Requester.py | 50 ++- github/Requester.pyi | 32 +- github/SelfHostedActionsRunner.pyi | 38 +-- github/__init__.py | 5 +- github/__init__.pyi | 5 +- tests/Authentication.py | 10 + tests/Framework.py | 22 ++ tests/GithubIntegration.py | 314 ++++++++++-------- .../Authentication.testAppAuthentication.txt | 21 ++ .../GithubIntegration.testGetAccessToken.txt | 32 ++ ...on.testGetAccessTokenForNoInstallation.txt | 11 + ...ation.testGetAccessTokenWithExpiredJWT.txt | 11 + ...tion.testGetAccessTokenWithInvalidData.txt | 11 + ...stGetAccessTokenWithInvalidPermissions.txt | 10 + ...thubIntegration.testGetAppInstallation.txt | 11 + ...ntegration.testGetInstallationNotFound.txt | 10 + ...tion.testGetInstallationWithExpiredJWT.txt | 11 + ...GithubIntegration.testGetInstallations.txt | 11 + ...thubIntegration.testGetOrgInstallation.txt | 11 + ...hubIntegration.testGetRepoInstallation.txt | 10 + ...hubIntegration.testGetUserInstallation.txt | 10 + 30 files changed, 817 insertions(+), 304 deletions(-) create mode 100644 github/AppAuthentication.py create mode 100644 github/AppAuthentication.pyi create mode 100644 github/GithubIntegration.py create mode 100644 github/GithubIntegration.pyi create mode 100644 tests/ReplayData/Authentication.testAppAuthentication.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetAccessToken.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetAccessTokenForNoInstallation.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetAccessTokenWithExpiredJWT.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetAccessTokenWithInvalidData.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetAccessTokenWithInvalidPermissions.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetAppInstallation.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetInstallationNotFound.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetInstallationWithExpiredJWT.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetInstallations.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetOrgInstallation.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetRepoInstallation.txt create mode 100644 tests/ReplayData/GithubIntegration.testGetUserInstallation.txt diff --git a/github/AppAuthentication.py b/github/AppAuthentication.py new file mode 100644 index 00000000..c6527b92 --- /dev/null +++ b/github/AppAuthentication.py @@ -0,0 +1,41 @@ +############################ Copyrights and license ############################ +# # +# Copyright 2023 Denis Blanchette # +# # +# This file is part of PyGithub. # +# http://pygithub.readthedocs.io/ # +# # +# PyGithub is free software: you can redistribute it and/or modify it under # +# the terms of the GNU Lesser General Public License as published by the Free # +# Software Foundation, either version 3 of the License, or (at your option) # +# any later version. # +# # +# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY # +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # +# details. # +# # +# You should have received a copy of the GNU Lesser General Public License # +# along with PyGithub. If not, see . # +# # +################################################################################ + + +class AppAuthentication: + def __init__( + self, + app_id, + private_key, + installation_id, + token_permissions=None, + ): + assert isinstance(app_id, (int, str)), app_id + assert isinstance(private_key, str) + assert isinstance(installation_id, int), installation_id + assert token_permissions is None or isinstance( + token_permissions, dict + ), token_permissions + self.app_id = app_id + self.private_key = private_key + self.installation_id = installation_id + self.token_permissions = token_permissions diff --git a/github/AppAuthentication.pyi b/github/AppAuthentication.pyi new file mode 100644 index 00000000..040b5f02 --- /dev/null +++ b/github/AppAuthentication.pyi @@ -0,0 +1,10 @@ +from typing import Optional, Dict, Union + +class AppAuthentication: + def __init__( + self, + app_id: Union[int, str], + private_key: str, + installation_id: int, + token_permissions: Optional[Dict[str, str]] = ..., + ): ... diff --git a/github/Consts.py b/github/Consts.py index 6f2d9bcb..b4b7bdd0 100644 --- a/github/Consts.py +++ b/github/Consts.py @@ -131,3 +131,20 @@ deploymentStatusEnhancementsPreview = "application/vnd.github.flash-preview+json # https://developer.github.com/changes/2019-12-03-internal-visibility-changes/ repoVisibilityPreview = "application/vnd.github.nebula-preview+json" + +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 + +# JWT expiry in seconds. Could be set for max 600 seconds (10 minutes). +# https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app +DEFAULT_JWT_EXPIRY = 300 +MIN_JWT_EXPIRY = 15 +MAX_JWT_EXPIRY = 600 +# https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#generating-a-json-web-token-jwt +# "The time the JWT was created. To protect against clock drift, we recommend you set this 60 seconds in the past." +DEFAULT_JWT_ISSUED_AT = -60 diff --git a/github/GithubIntegration.py b/github/GithubIntegration.py new file mode 100644 index 00000000..ecc136c6 --- /dev/null +++ b/github/GithubIntegration.py @@ -0,0 +1,199 @@ +import time + +import deprecated +import jwt + +from github import Consts +from github.GithubException import GithubException +from github.Installation import Installation +from github.InstallationAuthorization import InstallationAuthorization +from github.PaginatedList import PaginatedList +from github.Requester import Requester + + +class GithubIntegration: + """ + Main class to obtain tokens for a GitHub integration. + """ + + def __init__( + self, + integration_id, + private_key, + base_url=Consts.DEFAULT_BASE_URL, + jwt_expiry=Consts.DEFAULT_JWT_EXPIRY, + jwt_issued_at=Consts.DEFAULT_JWT_ISSUED_AT, + ): + """ + :param integration_id: int + :param private_key: string + :param base_url: string + :param jwt_expiry: int. Expiry of the JWT used to get the information about this integration. + The default expiration is in 5 minutes and is capped at 10 minutes according to GitHub documentation + https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#generating-a-json-web-token-jwt + :param jwt_issued_at: int. Number of seconds, relative to now, to set for the "iat" (issued at) parameter. + The default value is -60 to protect against clock drift + """ + assert isinstance(integration_id, (int, str)), integration_id + assert isinstance(private_key, str), "supplied private key should be a string" + assert isinstance(base_url, str), base_url + assert isinstance(jwt_expiry, int), jwt_expiry + assert Consts.MIN_JWT_EXPIRY <= jwt_expiry <= Consts.MAX_JWT_EXPIRY, jwt_expiry + assert isinstance(jwt_issued_at, int) + + self.base_url = base_url + self.integration_id = integration_id + self.private_key = private_key + self.jwt_expiry = jwt_expiry + self.jwt_issued_at = jwt_issued_at + self.__requester = Requester( + login_or_token=None, + password=None, + jwt=self.create_jwt(), + app_auth=None, + base_url=self.base_url, + timeout=Consts.DEFAULT_TIMEOUT, + user_agent="PyGithub/Python", + per_page=Consts.DEFAULT_PER_PAGE, + verify=True, + retry=None, + pool_size=None, + ) + + def _get_headers(self): + """ + Get headers for the requests. + + :return: dict + """ + return { + "Authorization": f"Bearer {self.create_jwt()}", + "Accept": Consts.mediaTypeIntegrationPreview, + "User-Agent": "PyGithub/Python", + } + + def _get_installed_app(self, url): + """ + Get installation for the given URL. + + :param url: str + :rtype: :class:`github.Installation.Installation` + """ + headers, response = self.__requester.requestJsonAndCheck( + "GET", url, headers=self._get_headers() + ) + + return Installation( + requester=self.__requester, + headers=headers, + attributes=response, + completed=True, + ) + + def create_jwt(self): + """ + Create a signed JWT + https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app + + :return string: + """ + now = int(time.time()) + payload = { + "iat": now + self.jwt_issued_at, + "exp": now + self.jwt_expiry, + "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, permissions=None): + """ + :calls: `POST /app/installations/{installation_id}/access_tokens ` + :param installation_id: int + :param permissions: dict + :return: :class:`github.InstallationAuthorization.InstallationAuthorization` + """ + if permissions is None: + permissions = {} + + if not isinstance(permissions, dict): + raise GithubException( + status=400, data={"message": "Invalid permissions"}, headers=None + ) + + body = {"permissions": permissions} + headers, response = self.__requester.requestJsonAndCheck( + "POST", + f"/app/installations/{installation_id}/access_tokens", + input=body, + ) + + return InstallationAuthorization( + requester=self.__requester, + headers=headers, + attributes=response, + completed=True, + ) + + @deprecated.deprecated("Use get_repo_installation") + def get_installation(self, owner, repo): + """ + Deprecated by get_repo_installation + + :calls: `GET /repos/{owner}/{repo}/installation ` + :param owner: str + :param repo: str + :rtype: :class:`github.Installation.Installation` + """ + return self._get_installed_app(url=f"/repos/{owner}/{repo}/installation") + + def get_installations(self): + """ + :calls: GET /app/installations + :rtype: :class:`github.PaginatedList.PaginatedList[github.Installation.Installation]` + """ + return PaginatedList( + contentClass=Installation, + requester=self.__requester, + firstUrl="/app/installations", + firstParams=None, + headers=self._get_headers(), + list_item="installations", + ) + + def get_org_installation(self, org): + """ + :calls: `GET /orgs/{org}/installation ` + :param org: str + :rtype: :class:`github.Installation.Installation` + """ + return self._get_installed_app(url=f"/orgs/{org}/installation") + + def get_repo_installation(self, owner, repo): + """ + :calls: `GET /repos/{owner}/{repo}/installation ` + :param owner: str + :param repo: str + :rtype: :class:`github.Installation.Installation` + """ + return self._get_installed_app(url=f"/repos/{owner}/{repo}/installation") + + def get_user_installation(self, username): + """ + :calls: `GET /users/{username}/installation ` + :param username: str + :rtype: :class:`github.Installation.Installation` + """ + return self._get_installed_app(url=f"/users/{username}/installation") + + def get_app_installation(self, installation_id): + """ + :calls: `GET /app/installations/{installation_id} ` + :param installation_id: int + :rtype: :class:`github.Installation.Installation` + """ + return self._get_installed_app(url=f"/app/installations/{installation_id}") diff --git a/github/GithubIntegration.pyi b/github/GithubIntegration.pyi new file mode 100644 index 00000000..52a5c019 --- /dev/null +++ b/github/GithubIntegration.pyi @@ -0,0 +1,34 @@ +from typing import Union, Optional, Dict + +from github.Installation import Installation +from github.InstallationAuthorization import InstallationAuthorization +from github.PaginatedList import PaginatedList +from github.Requester import Requester + +class GithubIntegration: + integration_id: Union[int, str] = ... + private_key: str = ... + base_url: str = ... + jwt_expiry: int = ... + jwt_issued_at: int = ... + __requester: Requester = ... + def __init__( + self, + integration_id: Union[int, str], + private_key: str, + base_url: str = ..., + jwt_expiry: int = ..., + jwt_issued_at: int = ..., + ) -> None: ... + def _get_installed_app(self, url: str) -> Installation: ... + def _get_headers(self) -> Dict[str, str]: ... + def create_jwt(self, expiration: int = ...) -> str: ... + def get_access_token( + self, installation_id: int, permissions: Optional[Dict[str, str]] = ... + ) -> InstallationAuthorization: ... + def get_app_installation(self, installation_id: int) -> Installation: ... + def get_installation(self, owner: str, repo: str) -> Installation: ... + def get_installations(self) -> PaginatedList[Installation]: ... + def get_org_installation(self, org: str) -> Installation: ... + def get_repo_installation(self, owner: str, repo: str) -> Installation: ... + def get_user_installation(self, username: str) -> Installation: ... diff --git a/github/InstallationAuthorization.py b/github/InstallationAuthorization.py index 06c695f9..036a3eef 100644 --- a/github/InstallationAuthorization.py +++ b/github/InstallationAuthorization.py @@ -57,10 +57,26 @@ class InstallationAuthorization(github.GithubObject.NonCompletableGithubObject): """ return self._on_behalf_of.value + @property + def permissions(self): + """ + :type: dict + """ + return self._permissions.value + + @property + def repository_selection(self): + """ + :type: string + """ + return self._repository_selection.value + def _initAttributes(self): self._token = github.GithubObject.NotSet self._expires_at = github.GithubObject.NotSet self._on_behalf_of = github.GithubObject.NotSet + self._permissions = github.GithubObject.NotSet + self._repository_selection = github.GithubObject.NotSet def _useAttributes(self, attributes): if "token" in attributes: # pragma no branch @@ -71,3 +87,9 @@ class InstallationAuthorization(github.GithubObject.NonCompletableGithubObject): self._on_behalf_of = self._makeClassAttribute( github.NamedUser.NamedUser, attributes["on_behalf_of"] ) + if "permissions" in attributes: # pragma no branch + self._permissions = self._makeDictAttribute(attributes["permissions"]) + if "repository_selection" in attributes: # pragma no branch + self._repository_selection = self._makeStringAttribute( + attributes["repository_selection"] + ) diff --git a/github/InstallationAuthorization.pyi b/github/InstallationAuthorization.pyi index 545ff160..8ef8e38f 100644 --- a/github/InstallationAuthorization.pyi +++ b/github/InstallationAuthorization.pyi @@ -14,3 +14,7 @@ class InstallationAuthorization(NonCompletableGithubObject): def on_behalf_of(self) -> NamedUser: ... @property def token(self) -> str: ... + @property + def permissions(self) -> dict: ... + @property + def repository_selection(self) -> str: ... diff --git a/github/MainClass.py b/github/MainClass.py index 5b626ad2..5b438d6c 100644 --- a/github/MainClass.py +++ b/github/MainClass.py @@ -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//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 `_ - :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 diff --git a/github/MainClass.pyi b/github/MainClass.pyi index 62b84e23..63ccf786 100644 --- a/github/MainClass.pyi +++ b/github/MainClass.pyi @@ -2,6 +2,7 @@ from datetime import datetime from io import BytesIO from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union, overload +from github.AppAuthentication import AppAuthentication from github.AuthenticatedUser import AuthenticatedUser from github.Commit import Commit from github.ContentFile import ContentFile @@ -10,8 +11,6 @@ from github.Gist import Gist from github.GithubObject import GithubObject, _NotSetType from github.GitignoreTemplate import GitignoreTemplate from github.HookDescription import HookDescription -from github.Installation import Installation -from github.InstallationAuthorization import InstallationAuthorization from github.Issue import Issue from github.License import License from github.NamedUser import NamedUser @@ -32,10 +31,9 @@ class Github: login_or_token: Optional[str] = ..., password: Optional[str] = ..., jwt: Optional[str] = ..., + app_auth: Optional[AppAuthentication] = ..., base_url: str = ..., timeout: int = ..., - client_id: Optional[str] = ..., - client_secret: Optional[str] = ..., user_agent: str = ..., per_page: int = ..., verify: bool = ..., @@ -67,7 +65,6 @@ class Github: def get_gitignore_templates(self) -> List[str]: ... def get_hook(self, name: str) -> HookDescription: ... def get_hooks(self) -> List[HookDescription]: ... - def get_installation(self, id: int) -> Installation: ... def get_license(self, key: Union[str, _NotSetType] = ...) -> License: ... def get_licenses(self) -> PaginatedList[License]: ... def get_organization(self, login: str) -> Organization: ... @@ -87,7 +84,9 @@ class Github: @overload def get_user(self, login: _NotSetType = ...) -> AuthenticatedUser: ... @overload - def get_user(self, login: str) -> NamedUser: ... + def get_user( + self, login: Union[str, _NotSetType] = ... + ) -> Union[NamedUser, AuthenticatedUser]: ... def get_user_by_id(self, user_id: int) -> NamedUser: ... def get_users( self, since: Union[int, _NotSetType] = ... @@ -139,13 +138,3 @@ class Github: order: Union[str, _NotSetType] = ..., **qualifiers: Any ) -> PaginatedList[NamedUser]: ... - -class GithubIntegration: - def __init__( - self, integration_id: Union[int, str], private_key: str, base_url: str = ... - ) -> None: ... - def create_jwt(self, expiration: int = ...) -> str: ... - def get_access_token( - self, installation_id: int, user_id: Optional[int] = ... - ) -> InstallationAuthorization: ... - def get_installation(self, owner: str, repo: str) -> Installation: ... diff --git a/github/Requester.py b/github/Requester.py index 8c6e13ce..253fab62 100644 --- a/github/Requester.py +++ b/github/Requester.py @@ -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 diff --git a/github/Requester.pyi b/github/Requester.pyi index 381fae92..be3c6aa0 100644 --- a/github/Requester.pyi +++ b/github/Requester.pyi @@ -4,7 +4,9 @@ from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Union from requests.models import Response +from github.AppAuthentication import AppAuthentication from github.GithubObject import GithubObject +from github.InstallationAuthorization import InstallationAuthorization from urllib3.util import Retry @@ -47,6 +49,8 @@ class HTTPSRequestsConnectionClass: ) -> None: ... class Requester: + __installation_authorization: Optional[InstallationAuthorization] = ... + __app_auth: Optional[AppAuthentication] = ... def DEBUG_ON_RESPONSE( self, statusCode: int, responseHeader: Dict[str, str], data: str ) -> None: ... @@ -54,7 +58,7 @@ class Requester: def __check( self, status: int, - responseHeader: Dict[str, Any], + responseHeaders: Dict[str, Any], output: str, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: ... def __addParametersToUrl( @@ -65,7 +69,7 @@ class Requester: def __authenticate( self, url: str, - responseHeader: Dict[str, Any], + requestHeaders: Dict[str, Any], parameters: Dict[str, Any], ) -> None: ... def __customConnection( @@ -88,37 +92,37 @@ class Requester: requestHeaders: Dict[str, str], input: Optional[str], status: Optional[int], - responseHeader: Dict[str, Any], + responseHeaders: Dict[str, Any], output: Optional[str], ) -> None: ... def __makeAbsoluteUrl(self, url: str) -> str: ... def __structuredFromJson(self, data: str) -> Optional[Dict[str, Any]]: ... def __requestEncode( self, + cnx: Union[HTTPRequestsConnectionClass, HTTPSRequestsConnectionClass], verb: str, url: str, - parameters: Dict[str, str] = ..., - headers: Dict[str, str] = ..., - input: Optional[str] = ..., - encode: Callable[[str], str] = ..., + parameters: Dict[str, str], + requestHeaders: Dict[str, str], + input: Optional[str], + encode: Callable[[str], str], ) -> Tuple[int, Dict[str, Any], str]: ... def __requestRaw( self, + cnx: Union[HTTPRequestsConnectionClass, HTTPSRequestsConnectionClass], verb: str, url: str, - parameters: Dict[str, str] = ..., - headers: Dict[str, str] = ..., - input: Optional[str] = ..., + requestHeaders: Dict[str, str], + input: Optional[str], ) -> Tuple[int, Dict[str, Any], str]: ... def __init__( self, login_or_token: Optional[str], password: Optional[str], jwt: Optional[str], + app_auth: Optional[AppAuthentication], base_url: str, timeout: int, - client_id: Optional[str], - client_secret: Optional[str], user_agent: str, per_page: int, verify: bool, @@ -127,6 +131,10 @@ class Requester: ) -> None: ... def _initializeDebugFeature(self) -> None: ... def check_me(self, obj: GithubObject) -> None: ... + def _must_refresh_token(self) -> bool: ... + def _get_installation_authorization(self) -> InstallationAuthorization: ... + def _refresh_token_if_needed(self) -> None: ... + def _refresh_token(self) -> None: ... @classmethod def injectConnectionClasses( cls, httpConnectionClass: Callable, httpsConnectionClass: Callable diff --git a/github/SelfHostedActionsRunner.pyi b/github/SelfHostedActionsRunner.pyi index deb486f5..0ab60ba0 100644 --- a/github/SelfHostedActionsRunner.pyi +++ b/github/SelfHostedActionsRunner.pyi @@ -1,19 +1,19 @@ -from typing import Any, Dict, List, Union - -from github.GithubObject import NonCompletableGithubObject - -class SelfHostedActionsRunner(NonCompletableGithubObject): - def __repr__(self) -> str: ... - def _initAttributes(self) -> None: ... - def _useAttributes(self, attributes: Dict[str, Any]) -> None: ... - @property - def id(self) -> int: ... - @property - def name(self) -> str: ... - @property - def os(self) -> str: ... - @property - def status(self) -> str: ... - @property - def busy(self) -> bool: ... - def labels(self) -> List[Dict[str, Union[str, int]]]: ... +from typing import Any, Dict, List, Union + +from github.GithubObject import NonCompletableGithubObject + +class SelfHostedActionsRunner(NonCompletableGithubObject): + def __repr__(self) -> str: ... + def _initAttributes(self) -> None: ... + def _useAttributes(self, attributes: Dict[str, Any]) -> None: ... + @property + def id(self) -> int: ... + @property + def name(self) -> str: ... + @property + def os(self) -> str: ... + @property + def status(self) -> str: ... + @property + def busy(self) -> bool: ... + def labels(self) -> List[Dict[str, Union[str, int]]]: ... diff --git a/github/__init__.py b/github/__init__.py index c978fbe9..0e3227de 100644 --- a/github/__init__.py +++ b/github/__init__.py @@ -35,6 +35,7 @@ like :class:`github.NamedUser.NamedUser` or :class:`github.Repository.Repository All classes inherit from :class:`github.GithubObject.GithubObject`. """ __all__ = [ + "AppAuthentication", "BadAttributeException", "BadCredentialsException", "BadUserAgentException", @@ -53,7 +54,9 @@ __all__ = [ import logging -from github.MainClass import Github, GithubIntegration +from github.AppAuthentication import AppAuthentication +from github.GithubIntegration import GithubIntegration +from github.MainClass import Github from .GithubException import ( BadAttributeException, diff --git a/github/__init__.pyi b/github/__init__.pyi index a3f3a5ff..96009018 100644 --- a/github/__init__.pyi +++ b/github/__init__.pyi @@ -1,11 +1,12 @@ +from github.AppAuthentication import AppAuthentication as AppAuthentication +from github.GithubIntegration import GithubIntegration as GithubIntegration from github.MainClass import Github as Github -from github.MainClass import GithubIntegration as GithubIntegration from .GithubException import BadAttributeException as BadAttributeException from .GithubException import BadCredentialsException as BadCredentialsException from .GithubException import BadUserAgentException as BadUserAgentException from .GithubException import GithubException as GithubException -from .GithubException import IncompletableObject as IncompleteableObject +from .GithubException import IncompletableObject as IncompletableObject from .GithubException import RateLimitExceededException as RateLimitExceededException from .GithubException import TwoFactorException as TwoFactorException from .GithubException import UnknownObjectException as UnknownObjectException diff --git a/tests/Authentication.py b/tests/Authentication.py index cc65af30..c66dd731 100644 --- a/tests/Authentication.py +++ b/tests/Authentication.py @@ -52,6 +52,16 @@ class Authentication(Framework.BasicTestCase): g = github.Github(user_agent="PyGithubTester") self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + def testAppAuthentication(self): + g = github.Github( + app_auth=github.AppAuthentication( + app_id=self.app_id, + private_key=self.app_private_key, + installation_id=29782936, + ), + ) + self.assertEqual(g.get_user("ammarmallik").name, "Ammar Akbar") + def testAuthorizationHeaderWithLogin(self): # See special case in Framework.fixAuthorizationHeader g = github.Github("fake_login", "fake_password") diff --git a/tests/Framework.py b/tests/Framework.py index 2b9215ac..24ab745a 100644 --- a/tests/Framework.py +++ b/tests/Framework.py @@ -46,6 +46,24 @@ from urllib3.util import Url # type: ignore import github +APP_PRIVATE_KEY = """ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC+5ePolLv6VcWLp2f17g6r6vHl+eoLuodOOfUl8JK+MVmvXbPa +xDy0SS0pQhwTOMtB0VdSt++elklDCadeokhEoGDQp411o+kiOhzLxfakp/kewf4U +HJnu4M/A2nHmxXVe2lzYnZvZHX5BM4SJo5PGdr0Ue2JtSXoAtYr6qE9maQIDAQAB +AoGAFhOJ7sy8jG+837Clcihso+8QuHLVYTPaD+7d7dxLbBlS8NfaQ9Nr3cGUqm/N +xV9NCjiGa7d/y4w/vrPwGh6UUsA+CvndwDgBd0S3WgIdWvAvHM8wKgNh/GBLLzhT +Bg9BouRUzcT1MjAnkGkWqqCAgN7WrCSUMLt57TNleNWfX90CQQDjvVKTT3pOiavD +3YcLxwkyeGd0VMvKiS4nV0XXJ97cGXs2GpOGXldstDTnF5AnB6PbukdFLHpsx4sW +Hft3LRWnAkEA1pY15ke08wX6DZVXy7zuQ2izTrWSGySn7B41pn55dlKpttjHeutA +3BEQKTFvMhBCphr8qST7Wf1SR9FgO0tFbwJAEhHji2yy96hUyKW7IWQZhrem/cP8 +p4Va9CQolnnDZRNgg1p4eiDiLu3dhLiJ547joXuWTBbLX/Y1Qvv+B+a74QJBAMCW +O3WbMZlS6eK6//rIa4ZwN00SxDg8I8FUM45jwBsjgVGrKQz2ilV3sutlhIiH82kk +m1Iq8LMJGYl/LkDJA10CQBV1C+Xu3ukknr7C4A/4lDCa6Xb27cr1HanY7i89A+Ab +eatdM6f/XVqWp8uPT9RggUV9TjppJobYGT2WrWJMkYw= +-----END RSA PRIVATE KEY----- +""" + def readLine(file_): line = file_.readline() @@ -268,6 +286,8 @@ class BasicTestCase(unittest.TestCase): self.password = GithubCredentials.password self.oauth_token = GithubCredentials.oauth_token self.jwt = GithubCredentials.jwt + self.app_id = GithubCredentials.app_id + self.app_private_key = GithubCredentials.app_private_key else: github.Requester.Requester.injectConnectionClasses( lambda ignored, *args, **kwds: ReplayingHttpConnection( @@ -281,6 +301,8 @@ class BasicTestCase(unittest.TestCase): self.password = "password" self.oauth_token = "oauth_token" self.jwt = "jwt" + self.app_id = 123456 + self.app_private_key = APP_PRIVATE_KEY httpretty.enable(allow_net_connect=False) diff --git a/tests/GithubIntegration.py b/tests/GithubIntegration.py index 272f50db..37b14344 100644 --- a/tests/GithubIntegration.py +++ b/tests/GithubIntegration.py @@ -1,15 +1,15 @@ -import datetime -import json import sys import time # NOQA -import unittest import jwt import requests # NOQA -from github.GithubObject import GithubObject +import github -private_key = """ +from . import Framework + +APP_ID = 243473 +PRIVATE_KEY = """ -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQC+5ePolLv6VcWLp2f17g6r6vHl+eoLuodOOfUl8JK+MVmvXbPa xDy0SS0pQhwTOMtB0VdSt++elklDCadeokhEoGDQp411o+kiOhzLxfakp/kewf4U @@ -26,8 +26,7 @@ m1Iq8LMJGYl/LkDJA10CQBV1C+Xu3ukknr7C4A/4lDCa6Xb27cr1HanY7i89A+Ab eatdM6f/XVqWp8uPT9RggUV9TjppJobYGT2WrWJMkYw= -----END RSA PRIVATE KEY----- """ - -public_key = """ +PUBLIC_KEY = """ -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+5ePolLv6VcWLp2f17g6r6vHl +eoLuodOOfUl8JK+MVmvXbPaxDy0SS0pQhwTOMtB0VdSt++elklDCadeokhEoGDQ @@ -37,154 +36,189 @@ e2JtSXoAtYr6qE9maQIDAQAB """ -class GithubIntegration(unittest.TestCase): +class GithubIntegration(Framework.BasicTestCase): def setUp(self): - # This flag ask requester to do some checking, - # for debug and test purpose. But - # `InstallationAuthorization.InstallationAuthorization` is a - # `NonCompletableGithubObject`, it does not have requester. - # So the check is not needed. - # see `GithubIntegration.get_access_token` - self.origin_check_after_init_flag = GithubObject.CHECK_AFTER_INIT_FLAG - GithubObject.setCheckAfterInitFlag(False) - - self.origin_time = sys.modules["time"].time - sys.modules["time"].time = lambda: 1550055331.7435968 - - class Mock: - def __init__(self): - self.args = tuple() - self.kwargs = dict() - - @property - def status_code(self): - return 201 - - def json(self): - return json.loads(self.text) - - @property - def text(self): - return ( - '{"token": "v1.ce63424bc55028318325caac4f4c3a5378ca0038",' - '"expires_at": "2019-02-13T11:10:38Z"}' - ) - - def __call__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - return self - - self.origin_request_post = sys.modules["requests"].post - self.mock = Mock() - sys.modules["requests"].post = self.mock - - class GetMock: - def __init__(self): - self.args = tuple() - self.kwargs = dict() - self.calls = [] - - @property - def status_code(self): - return 201 - - def json(self): - return json.loads(self.text) - - @property - def text(self): - return ( - '{"id":111111,"account":{"login":"foo","id":11111111,' - '"node_id":"foobar",' - '"avatar_url":"https://avatars3.githubusercontent.com/u/11111111?v=4",' - '"gravatar_id":"","url":"https://api.github.com/users/foo",' - '"html_url":"https://github.com/foo",' - '"followers_url":"https://api.github.com/users/foo/followers",' - '"following_url":"https://api.github.com/users/foo/following{/other_user}",' - '"gists_url":"https://api.github.com/users/foo/gists{/gist_id}",' - '"starred_url":"https://api.github.com/users/foo/starred{/owner}{/repo}",' - '"subscriptions_url":"https://api.github.com/users/foo/subscriptions",' - '"organizations_url":"https://api.github.com/users/foo/orgs",' - '"repos_url":"https://api.github.com/users/foo/repos",' - '"events_url":"https://api.github.com/users/foo/events{/privacy}",' - '"received_events_url":"https://api.github.com/users/foo/received_events",' - '"type":"Organization","site_admin":false},"repository_selection":"all",' - '"access_tokens_url":"https://api.github.com/app/installations/111111/access_tokens",' - '"repositories_url":"https://api.github.com/installation/repositories",' - '"html_url":"https://github.com/organizations/foo/settings/installations/111111",' - '"app_id":11111,"target_id":11111111,"target_type":"Organization",' - '"permissions":{"issues":"write","pull_requests":"write","statuses":"write","contents":"read",' - '"metadata":"read"},"events":["pull_request","release"],"created_at":"2019-04-17T16:10:37.000Z",' - '"updated_at":"2019-05-03T06:27:48.000Z","single_file_name":null}' - ) - - def __call__(self, *args, **kwargs): - self.calls.append((args, kwargs)) - self.args = args - self.kwargs = kwargs - return self - - self.origin_request_get = sys.modules["requests"].get - self.get_mock = GetMock() - sys.modules["requests"].get = self.get_mock + super().setUp() + self.org_installation_id = 30614487 + self.repo_installation_id = 30614431 + self.user_installation_id = 30614431 def testCreateJWT(self): - from github import GithubIntegration - - integration = GithubIntegration(25216, private_key) - token = integration.create_jwt() + self.origin_time = sys.modules["time"].time + sys.modules["time"].time = lambda: 1550055331.7435968 + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + token = github_integration.create_jwt() payload = jwt.decode( token, - key=public_key, + key=PUBLIC_KEY, algorithms=["RS256"], options={"verify_exp": False}, ) self.assertDictEqual( - payload, {"iat": 1550055331, "exp": 1550055391, "iss": 25216} + payload, {"iat": 1550055271, "exp": 1550055631, "iss": APP_ID} ) + sys.modules["time"].time = self.origin_time + + def testGetInstallations(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + installations = github_integration.get_installations() + + self.assertEqual(len(list(installations)), 2) + self.assertEqual(installations[0].id, self.org_installation_id) + self.assertEqual(installations[1].id, self.repo_installation_id) def testGetAccessToken(self): - from github import GithubIntegration - - integration = GithubIntegration(25216, private_key) - auth_obj = integration.get_access_token(664281) - self.assertEqual( - self.mock.args[0], - "https://api.github.com/app/installations/664281/access_tokens", - ) - self.assertEqual(auth_obj.token, "v1.ce63424bc55028318325caac4f4c3a5378ca0038") - self.assertEqual( - auth_obj.expires_at, datetime.datetime(2019, 2, 13, 11, 10, 38) - ) - self.assertEqual( - repr(auth_obj), "InstallationAuthorization(expires_at=2019-02-13 11:10:38)" + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY ) - def test_get_installation(self): - from github import GithubIntegration - - integr = GithubIntegration("11111", private_key) - inst = integr.get_installation("foo", "bar") - self.assertEqual( - self.get_mock.calls[0][0], - ("https://api.github.com/repos/foo/bar/installation",), + # Get repo installation access token + repo_installation_authorization = github_integration.get_access_token( + self.repo_installation_id ) - self.assertEqual(inst.id, 111111) - - def test_get_installation_custom_base_url(self): - from github import GithubIntegration - - integr = GithubIntegration("11111", private_key, base_url="https://corp.com/v3") - inst = integr.get_installation("foo", "bar") self.assertEqual( - self.get_mock.calls[0][0], - ("https://corp.com/v3/repos/foo/bar/installation",), + repo_installation_authorization.token, + "ghs_1llwuELtXN5HDOB99XhpcTXdJxbOuF0ZlSmj", + ) + self.assertDictEqual( + repo_installation_authorization.permissions, + {"issues": "read", "metadata": "read"}, + ) + self.assertEqual( + repo_installation_authorization.repository_selection, "selected" ) - self.assertEqual(inst.id, 111111) - def tearDown(self): - GithubObject.setCheckAfterInitFlag(self.origin_check_after_init_flag) - sys.modules["time"].time = self.origin_time - sys.modules["requests"].post = self.origin_request_post - sys.modules["requests"].get = self.origin_request_get + # Get org installation access token + org_installation_authorization = github_integration.get_access_token( + self.org_installation_id + ) + self.assertEqual( + org_installation_authorization.token, + "ghs_V0xygF8yACXSDz5FM65QWV1BT2vtxw0cbgPw", + ) + org_permissions = { + "administration": "write", + "issues": "write", + "metadata": "read", + "organization_administration": "read", + } + self.assertDictEqual( + org_installation_authorization.permissions, org_permissions + ) + self.assertEqual( + org_installation_authorization.repository_selection, "selected" + ) + + # Get user installation access token + user_installation_authorization = github_integration.get_access_token( + self.user_installation_id + ) + self.assertEqual( + user_installation_authorization.token, + "ghs_1llwuELtXN5HDOB99XhpcTXdJxbOuF0ZlSmj", + ) + self.assertDictEqual( + user_installation_authorization.permissions, + {"issues": "read", "metadata": "read"}, + ) + self.assertEqual( + user_installation_authorization.repository_selection, "selected" + ) + + def testGetUserInstallation(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + installation = github_integration.get_user_installation(username="ammarmallik") + + self.assertEqual(installation.id, self.user_installation_id) + + def testGetOrgInstallation(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + installation = github_integration.get_org_installation(org="GithubApp-Test-Org") + + self.assertEqual(installation.id, self.org_installation_id) + + def testGetRepoInstallation(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + installation = github_integration.get_repo_installation( + owner="ammarmallik", repo="test-runner" + ) + + self.assertEqual(installation.id, self.repo_installation_id) + + def testGetAppInstallation(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + installation = github_integration.get_app_installation( + installation_id=self.org_installation_id + ) + + self.assertEqual(installation.id, self.org_installation_id) + + def testGetInstallationNotFound(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + with self.assertRaises(github.UnknownObjectException) as raisedexp: + github_integration.get_org_installation(org="GithubApp-Test-Org-404") + + self.assertEqual(raisedexp.exception.status, 404) + + def testGetInstallationWithExpiredJWT(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + with self.assertRaises(github.GithubException) as raisedexp: + github_integration.get_org_installation(org="GithubApp-Test-Org") + + self.assertEqual(raisedexp.exception.status, 401) + + def testGetAccessTokenWithExpiredJWT(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + with self.assertRaises(github.GithubException) as raisedexp: + github_integration.get_access_token(self.repo_installation_id) + + self.assertEqual(raisedexp.exception.status, 401) + + def testGetAccessTokenForNoInstallation(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + with self.assertRaises(github.UnknownObjectException) as raisedexp: + github_integration.get_access_token(40432121) + + self.assertEqual(raisedexp.exception.status, 404) + + def testGetAccessTokenWithInvalidPermissions(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + with self.assertRaises(github.GithubException) as raisedexp: + github_integration.get_access_token( + self.repo_installation_id, permissions={"test-permissions": "read"} + ) + + self.assertEqual(raisedexp.exception.status, 422) + + def testGetAccessTokenWithInvalidData(self): + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + with self.assertRaises(github.GithubException) as raisedexp: + github_integration.get_access_token( + self.repo_installation_id, permissions="invalid_data" + ) + + self.assertEqual(raisedexp.exception.status, 400) diff --git a/tests/ReplayData/Authentication.testAppAuthentication.txt b/tests/ReplayData/Authentication.testAppAuthentication.txt new file mode 100644 index 00000000..53391560 --- /dev/null +++ b/tests/ReplayData/Authentication.testAppAuthentication.txt @@ -0,0 +1,21 @@ +https +POST +api.github.com +None +/app/installations/29782936/access_tokens +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +201 +[('status', '201 CREATED'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"token":"ghs_1llwuELtXN5HDOB99XhpcTXdJxbOuF0ZlSmj", "expires_at":"2024-11-25T01:00:02Z", "permissions":{"issues":"read","metadata":"read"}, "repository_selection":"selected"} + +https +GET +api.github.com +None +/users/ammarmallik +{'Authorization': 'token private_token_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '57'), ('content-length', '1338'), ('server', 'GitHub.com'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '60'), ('etag', 'W/"e5e65462690241eb7d9bc213cc00b3df2c75984d29a36b2ee8e151deaf4f3981"'), ('date', 'Tue, 25 Oct 2022 02:01:06 GMT'), ('x-ratelimit-reset', '1666666583'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-resource', 'core'), ('x-ratelimit-used', '3'), ('accept-ranges', 'bytes'), ('x-github-request-id', 'D8C1:7CE1:C3DF20:D546B4:63574361'), ('content-security-policy', "default-src 'none'"), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-xss-protection', '0'), ('x-content-type-options', 'nosniff'), ('x-frame-options', 'deny'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('access-control-allow-origin', '*'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('x-github-media-type', 'github.v3; format=jsom'), ('last-modified', 'Mon, 24 Oct 2022 20:26:22 GMT'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept, Accept-Encoding, Accept, X-Requested-With')] +{"login":"ammarmallik","id":29196434,"node_id":"MDQ6VXNlcjI5MTk2NDM0","avatar_url":"https://avatars.githubusercontent.com/u/29196434?v=4","gravatar_id":"","url":"https://api.github.com/users/ammarmallik","html_url":"https://github.com/ammarmallik","followers_url":"https://api.github.com/users/ammarmallik/followers","following_url":"https://api.github.com/users/ammarmallik/following{/other_user}","gists_url":"https://api.github.com/users/ammarmallik/gists{/gist_id}","starred_url":"https://api.github.com/users/ammarmallik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ammarmallik/subscriptions","organizations_url":"https://api.github.com/users/ammarmallik/orgs","repos_url":"https://api.github.com/users/ammarmallik/repos","events_url":"https://api.github.com/users/ammarmallik/events{/privacy}","received_events_url":"https://api.github.com/users/ammarmallik/received_events","type":"User","site_admin":false,"name":"Ammar Akbar","company":null,"blog":"","location":"Lahore","email":null,"hireable":true,"bio":null,"twitter_username":null,"public_repos":14,"public_gists":2,"followers":0,"following":3,"created_at":"2017-06-05T09:42:01Z","updated_at":"2022-10-24T20:26:22Z"} diff --git a/tests/ReplayData/GithubIntegration.testGetAccessToken.txt b/tests/ReplayData/GithubIntegration.testGetAccessToken.txt new file mode 100644 index 00000000..4f37651d --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetAccessToken.txt @@ -0,0 +1,32 @@ +https +POST +api.github.com +None +/app/installations/30614431/access_tokens +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +201 +[('status', '201 CREATED'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"token":"ghs_1llwuELtXN5HDOB99XhpcTXdJxbOuF0ZlSmj", "expires_at":"2024-11-25T01:00:02Z", "permissions":{"issues":"read","metadata":"read"}, "repository_selection":"selected"} + +https +POST +api.github.com +None +/app/installations/30614487/access_tokens +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +201 +[('status', '201 CREATED'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"token":"ghs_V0xygF8yACXSDz5FM65QWV1BT2vtxw0cbgPw", "expires_at":"2024-11-25T01:00:02Z", "permissions":{"organization_administration":"read","administration":"write","issues":"write","metadata":"read"}, "repository_selection":"selected"} + +https +POST +api.github.com +None +/app/installations/30614431/access_tokens +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +201 +[('status', '201 CREATED'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"token":"ghs_1llwuELtXN5HDOB99XhpcTXdJxbOuF0ZlSmj", "expires_at":"2024-11-25T01:00:02Z", "permissions":{"issues":"read","metadata":"read"}, "repository_selection":"selected"} diff --git a/tests/ReplayData/GithubIntegration.testGetAccessTokenForNoInstallation.txt b/tests/ReplayData/GithubIntegration.testGetAccessTokenForNoInstallation.txt new file mode 100644 index 00000000..db1f2cb8 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetAccessTokenForNoInstallation.txt @@ -0,0 +1,11 @@ +https +POST +api.github.com +None +/app/installations/40432121/access_tokens +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +404 +[('status', '404 NOT FOUND'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', '"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"message":"Not Found","documentation_url":"https://docs.github.com/rest/reference/apps#get-a-repository-installation-for-the-authenticated-app"} + diff --git a/tests/ReplayData/GithubIntegration.testGetAccessTokenWithExpiredJWT.txt b/tests/ReplayData/GithubIntegration.testGetAccessTokenWithExpiredJWT.txt new file mode 100644 index 00000000..0ce4c847 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetAccessTokenWithExpiredJWT.txt @@ -0,0 +1,11 @@ +https +POST +api.github.com +None +/app/installations/30614431/access_tokens +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +401 +[('status', '401 UNAUTHORIZED'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"message":"'Expiration time' claim ('exp') must be a numeric value representing the future time at which the assertion expires","documentation_url":"https://docs.github.com/rest"} + diff --git a/tests/ReplayData/GithubIntegration.testGetAccessTokenWithInvalidData.txt b/tests/ReplayData/GithubIntegration.testGetAccessTokenWithInvalidData.txt new file mode 100644 index 00000000..86a61d11 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetAccessTokenWithInvalidData.txt @@ -0,0 +1,11 @@ +https +POST +api.github.com +None +/app/installations/30614431/access_tokens +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": "invalid_data"} +400 +[('status', '400 BAD REQUEST'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"message":"Problems parsing JSON","documentation_url":"https://docs.github.com/rest/reference/apps#create-an-installation-access-token-for-an-app"} + diff --git a/tests/ReplayData/GithubIntegration.testGetAccessTokenWithInvalidPermissions.txt b/tests/ReplayData/GithubIntegration.testGetAccessTokenWithInvalidPermissions.txt new file mode 100644 index 00000000..a9b790c5 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetAccessTokenWithInvalidPermissions.txt @@ -0,0 +1,10 @@ +https +POST +api.github.com +None +/app/installations/30614431/access_tokens +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {"test-permissions": "read"}} +422 +[('status', '422 UNPROCESSABLE ENTITY'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"message":"The permissions requested are not granted to this installation.","documentation_url":"https://docs.github.com/rest/reference/apps#create-an-installation-access-token-for-an-app"} diff --git a/tests/ReplayData/GithubIntegration.testGetAppInstallation.txt b/tests/ReplayData/GithubIntegration.testGetAppInstallation.txt new file mode 100644 index 00000000..c7367f31 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetAppInstallation.txt @@ -0,0 +1,11 @@ +https +GET +api.github.com +None +/app/installations/30614487 +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.machine-man-preview+json'} +None +200 +[('status', '200 OK'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', '"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"id":30614487,"account":{"login":"GithubApp-Test-Org","id":116723333,"node_id":"O_kgDOBvUOhQ","avatar_url":"https://avatars.githubusercontent.com/u/116723333?v=4","gravatar_id":"","url":"https://api.github.com/users/GithubApp-Test-Org","html_url":"https://github.com/GithubApp-Test-Org","followers_url":"https://api.github.com/users/GithubApp-Test-Org/followers","following_url":"https://api.github.com/users/GithubApp-Test-Org/following{/other_user}","gists_url":"https://api.github.com/users/GithubApp-Test-Org/gists{/gist_id}","starred_url":"https://api.github.com/users/GithubApp-Test-Org/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GithubApp-Test-Org/subscriptions","organizations_url":"https://api.github.com/users/GithubApp-Test-Org/orgs","repos_url":"https://api.github.com/users/GithubApp-Test-Org/repos","events_url":"https://api.github.com/users/GithubApp-Test-Org/events{/privacy}","received_events_url":"https://api.github.com/users/GithubApp-Test-Org/received_events","type":"Organization","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/30614487/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/organizations/GithubApp-Test-Org/settings/installations/30614487","app_id":243473,"app_slug":"gh-actions-test-ammar","target_id":116723333,"target_type":"Organization","permissions":{"issues":"write","metadata":"read","administration":"write","organization_administration":"read"},"events":[],"created_at":"2022-10-26T11:15:21.000Z","updated_at":"2022-10-26T11:36:34.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null} + diff --git a/tests/ReplayData/GithubIntegration.testGetInstallationNotFound.txt b/tests/ReplayData/GithubIntegration.testGetInstallationNotFound.txt new file mode 100644 index 00000000..ca39bc50 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetInstallationNotFound.txt @@ -0,0 +1,10 @@ +https +GET +api.github.com +None +/orgs/GithubApp-Test-Org-404/installation +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.machine-man-preview+json'} +None +404 +[('status', '404 NOT FOUND'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', '"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"message":"Not Found","documentation_url":"https://docs.github.com/rest/reference/apps#get-a-repository-installation-for-the-authenticated-app"} diff --git a/tests/ReplayData/GithubIntegration.testGetInstallationWithExpiredJWT.txt b/tests/ReplayData/GithubIntegration.testGetInstallationWithExpiredJWT.txt new file mode 100644 index 00000000..aae0ca58 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetInstallationWithExpiredJWT.txt @@ -0,0 +1,11 @@ +https +GET +api.github.com +None +/orgs/GithubApp-Test-Org/installation +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.machine-man-preview+json'} +None +401 +[('status', '401 UNAUTHORIZED'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"message":"'Expiration time' claim ('exp') must be a numeric value representing the future time at which the assertion expires","documentation_url":"https://docs.github.com/rest"} + diff --git a/tests/ReplayData/GithubIntegration.testGetInstallations.txt b/tests/ReplayData/GithubIntegration.testGetInstallations.txt new file mode 100644 index 00000000..762608ab --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetInstallations.txt @@ -0,0 +1,11 @@ +https +GET +api.github.com +None +/app/installations +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.machine-man-preview+json'} +None +200 +[('status', '200 OK'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', '"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +[{"id":30614487,"account":{"login":"GithubApp-Test-Org","id":116723333,"node_id":"O_kgDOBvUOhQ","avatar_url":"https://avatars.githubusercontent.com/u/116723333?v=4","gravatar_id":"","url":"https://api.github.com/users/GithubApp-Test-Org","html_url":"https://github.com/GithubApp-Test-Org","followers_url":"https://api.github.com/users/GithubApp-Test-Org/followers","following_url":"https://api.github.com/users/GithubApp-Test-Org/following{/other_user}","gists_url":"https://api.github.com/users/GithubApp-Test-Org/gists{/gist_id}","starred_url":"https://api.github.com/users/GithubApp-Test-Org/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GithubApp-Test-Org/subscriptions","organizations_url":"https://api.github.com/users/GithubApp-Test-Org/orgs","repos_url":"https://api.github.com/users/GithubApp-Test-Org/repos","events_url":"https://api.github.com/users/GithubApp-Test-Org/events{/privacy}","received_events_url":"https://api.github.com/users/GithubApp-Test-Org/received_events","type":"Organization","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/30614487/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/organizations/GithubApp-Test-Org/settings/installations/30614487","app_id":243473,"app_slug":"gh-actions-test-ammar","target_id":116723333,"target_type":"Organization","permissions":{"issues":"write","metadata":"read","administration":"write","organization_administration":"read"},"events":[],"created_at":"2022-10-26T11:15:21.000Z","updated_at":"2022-10-26T11:36:34.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null},{"id":30614431,"account":{"login":"ammarmallik","id":29196434,"node_id":"MDQ6VXNlcjI5MTk2NDM0","avatar_url":"https://avatars.githubusercontent.com/u/29196434?v=4","gravatar_id":"","url":"https://api.github.com/users/ammarmallik","html_url":"https://github.com/ammarmallik","followers_url":"https://api.github.com/users/ammarmallik/followers","following_url":"https://api.github.com/users/ammarmallik/following{/other_user}","gists_url":"https://api.github.com/users/ammarmallik/gists{/gist_id}","starred_url":"https://api.github.com/users/ammarmallik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ammarmallik/subscriptions","organizations_url":"https://api.github.com/users/ammarmallik/orgs","repos_url":"https://api.github.com/users/ammarmallik/repos","events_url":"https://api.github.com/users/ammarmallik/events{/privacy}","received_events_url":"https://api.github.com/users/ammarmallik/received_events","type":"User","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/30614431/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/settings/installations/30614431","app_id":243473,"app_slug":"gh-actions-test-ammar","target_id":29196434,"target_type":"User","permissions":{"issues":"write","metadata":"read","administration":"write"},"events":[],"created_at":"2022-10-26T11:13:03.000Z","updated_at":"2022-10-26T11:13:03.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null}] + diff --git a/tests/ReplayData/GithubIntegration.testGetOrgInstallation.txt b/tests/ReplayData/GithubIntegration.testGetOrgInstallation.txt new file mode 100644 index 00000000..229f1633 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetOrgInstallation.txt @@ -0,0 +1,11 @@ +https +GET +api.github.com +None +/orgs/GithubApp-Test-Org/installation +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.machine-man-preview+json'} +None +200 +[('status', '200 OK'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', '"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"id":30614487,"account":{"login":"GithubApp-Test-Org","id":116723333,"node_id":"O_kgDOBvUOhQ","avatar_url":"https://avatars.githubusercontent.com/u/116723333?v=4","gravatar_id":"","url":"https://api.github.com/users/GithubApp-Test-Org","html_url":"https://github.com/GithubApp-Test-Org","followers_url":"https://api.github.com/users/GithubApp-Test-Org/followers","following_url":"https://api.github.com/users/GithubApp-Test-Org/following{/other_user}","gists_url":"https://api.github.com/users/GithubApp-Test-Org/gists{/gist_id}","starred_url":"https://api.github.com/users/GithubApp-Test-Org/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GithubApp-Test-Org/subscriptions","organizations_url":"https://api.github.com/users/GithubApp-Test-Org/orgs","repos_url":"https://api.github.com/users/GithubApp-Test-Org/repos","events_url":"https://api.github.com/users/GithubApp-Test-Org/events{/privacy}","received_events_url":"https://api.github.com/users/GithubApp-Test-Org/received_events","type":"Organization","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/30614487/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/organizations/GithubApp-Test-Org/settings/installations/30614487","app_id":243473,"app_slug":"gh-actions-test-ammar","target_id":116723333,"target_type":"Organization","permissions":{"issues":"write","metadata":"read","administration":"write","organization_administration":"read"},"events":[],"created_at":"2022-10-26T11:15:21.000Z","updated_at":"2022-10-26T11:36:34.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null} + diff --git a/tests/ReplayData/GithubIntegration.testGetRepoInstallation.txt b/tests/ReplayData/GithubIntegration.testGetRepoInstallation.txt new file mode 100644 index 00000000..f50c0792 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetRepoInstallation.txt @@ -0,0 +1,10 @@ +https +GET +api.github.com +None +/repos/ammarmallik/test-runner/installation +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.machine-man-preview+json'} +None +200 +[('status', '200 OK'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', '"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"id":30614431,"account":{"login":"ammarmallik","id":29196434,"node_id":"MDQ6VXNlcjI5MTk2NDM0","avatar_url":"https://avatars.githubusercontent.com/u/29196434?v=4","gravatar_id":"","url":"https://api.github.com/users/ammarmallik","html_url":"https://github.com/ammarmallik","followers_url":"https://api.github.com/users/ammarmallik/followers","following_url":"https://api.github.com/users/ammarmallik/following{/other_user}","gists_url":"https://api.github.com/users/ammarmallik/gists{/gist_id}","starred_url":"https://api.github.com/users/ammarmallik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ammarmallik/subscriptions","organizations_url":"https://api.github.com/users/ammarmallik/orgs","repos_url":"https://api.github.com/users/ammarmallik/repos","events_url":"https://api.github.com/users/ammarmallik/events{/privacy}","received_events_url":"https://api.github.com/users/ammarmallik/received_events","type":"User","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/30614431/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/settings/installations/30614431","app_id":243473,"app_slug":"gh-actions-test-ammar","target_id":29196434,"target_type":"User","permissions":{"issues":"write","metadata":"read","administration":"write"},"events":[],"created_at":"2022-10-26T11:13:03.000Z","updated_at":"2022-10-26T11:13:03.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null} diff --git a/tests/ReplayData/GithubIntegration.testGetUserInstallation.txt b/tests/ReplayData/GithubIntegration.testGetUserInstallation.txt new file mode 100644 index 00000000..38f8723a --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetUserInstallation.txt @@ -0,0 +1,10 @@ +https +GET +api.github.com +None +/users/ammarmallik/installation +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.machine-man-preview+json'} +None +200 +[('status', '200 OK'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', '"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"id":30614431,"account":{"login":"ammarmallik","id":29196434,"node_id":"MDQ6VXNlcjI5MTk2NDM0","avatar_url":"https://avatars.githubusercontent.com/u/29196434?v=4","gravatar_id":"","url":"https://api.github.com/users/ammarmallik","html_url":"https://github.com/ammarmallik","followers_url":"https://api.github.com/users/ammarmallik/followers","following_url":"https://api.github.com/users/ammarmallik/following{/other_user}","gists_url":"https://api.github.com/users/ammarmallik/gists{/gist_id}","starred_url":"https://api.github.com/users/ammarmallik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ammarmallik/subscriptions","organizations_url":"https://api.github.com/users/ammarmallik/orgs","repos_url":"https://api.github.com/users/ammarmallik/repos","events_url":"https://api.github.com/users/ammarmallik/events{/privacy}","received_events_url":"https://api.github.com/users/ammarmallik/received_events","type":"User","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/30614431/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/settings/installations/30614431","app_id":243473,"app_slug":"gh-actions-test-ammar","target_id":29196434,"target_type":"User","permissions":{"issues":"write","metadata":"read","administration":"write"},"events":[],"created_at":"2022-10-26T11:13:03.000Z","updated_at":"2022-10-26T11:13:03.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null}