From fc2d0e150a53d90209b8f251ce686858449f12d0 Mon Sep 17 00:00:00 2001 From: Enrico Minack Date: Thu, 8 Jun 2023 09:17:25 +0200 Subject: [PATCH] Add authentication classes, move auth logic there (#2528) This adds argument `auth` and deprecates `login_or_token`, `password`, `jwt`, and `app_auth` arguments of `github.Github`. This adds argument `auth` and deprecates `integration_id`, `private_key` of `github.GithubIntegration`. This deprecates the `create_jwt` method of `github.GithubIntegration`, replaced by `github.Auth.AppAuth.create_jwt`. --- README.md | 12 +- doc/examples.rst | 1 + doc/examples/Authentication.rst | 67 ++++ doc/introduction.rst | 12 +- github/AppAuthentication.py | 25 +- github/ApplicationOAuth.py | 6 +- github/ApplicationOAuth.pyi | 1 + github/Auth.py | 342 ++++++++++++++++++ github/Consts.py | 3 + github/GithubIntegration.py | 91 ++--- github/GithubIntegration.pyi | 12 +- github/GithubObject.pyi | 1 + github/MainClass.py | 52 ++- github/MainClass.pyi | 2 + github/Requester.py | 118 +++--- github/Requester.pyi | 35 +- github/__init__.py | 2 + github/__init__.pyi | 1 + tests/Authentication.py | 123 ++++++- tests/Enterprise.py | 20 +- tests/Exceptions.py | 16 +- tests/Framework.py | 44 ++- tests/GithubIntegration.py | 132 +++---- tests/Issue134.py | 6 +- tests/Issue80.py | 5 +- tests/Logging_.py | 4 +- tests/PoolSize.py | 3 +- ...ication.testAppAuthTokenAuthentication.txt | 11 + ....testAppInstallationAuthAuthentication.txt | 21 ++ ...Authentication.testLoginAuthentication.txt | 11 + ...Authentication.testTokenAuthentication.txt | 11 + .../GithubIntegration.testAppAuth.txt | 11 + ...ithubIntegration.testDeprecatedAppAuth.txt | 11 + tests/Retry.py | 3 +- tox.ini | 1 + 35 files changed, 926 insertions(+), 290 deletions(-) create mode 100644 doc/examples/Authentication.rst create mode 100644 github/Auth.py create mode 100644 tests/ReplayData/Authentication.testAppAuthTokenAuthentication.txt create mode 100644 tests/ReplayData/Authentication.testAppInstallationAuthAuthentication.txt create mode 100644 tests/ReplayData/Authentication.testLoginAuthentication.txt create mode 100644 tests/ReplayData/Authentication.testTokenAuthentication.txt create mode 100644 tests/ReplayData/GithubIntegration.testAppAuth.txt create mode 100644 tests/ReplayData/GithubIntegration.testDeprecatedAppAuth.txt diff --git a/README.md b/README.md index 9e749abe..feca1385 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,19 @@ $ pip install PyGithub ```python from github import Github -# First create a Github instance: +# Authentication is defined via github.Auth +from github import Auth # using an access token -g = Github("access_token") +auth = Auth.Token("access_token") + +# First create a Github instance: + +# Public Web Github +g = Github(auth=auth) # Github Enterprise with custom hostname -g = Github(base_url="https://{hostname}/api/v3", login_or_token="access_token") +g = Github(base_url="https://{hostname}/api/v3", auth=auth) # Then play with your Github objects: for repo in g.get_user().get_repos(): diff --git a/doc/examples.rst b/doc/examples.rst index 1358e9a3..f1fd9b14 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -3,6 +3,7 @@ Examples .. toctree:: + examples/Authentication examples/MainClass examples/Repository examples/Branch diff --git a/doc/examples/Authentication.rst b/doc/examples/Authentication.rst new file mode 100644 index 00000000..7e6a7dda --- /dev/null +++ b/doc/examples/Authentication.rst @@ -0,0 +1,67 @@ +Authentication +============== + +Github supports various authentication methods. Depending on the entity that authenticates and the Github API endpoint +being called, only a subset of methods is available. + +All authentication methods require this import: + +.. code-block:: python + + >>> from github import Auth + +Login authentication +-------------------- + +Users can authenticate by a login and password: + +.. code-block:: python + + >>> auth = Auth.Login("user_login", "password") + >>> g = Github(auth=auth) + >>> g.get_user().login + 'user_login' + +OAuth token authentication +-------------------------- + +Users can authenticate by a token: + +.. code-block:: python + + >>> auth = Auth.Token("access_token") + >>> g = Github(auth=auth) + >>> g.get_user().login + 'login' + +App authentication +------------------ + +A Github Apps authenticate by an application id and a private key. + +Note that there is only a limited set of endpoints that can be called when authenticated as a Github App. +Instead of using ``github.Github``, entry point ``github.GithubIntegration`` should be used +when authenticated as a Github App: + +.. code-block:: python + + >>> auth = Auth.AppAuth(123456, private_key) + >>> gi = GithubIntegration(auth=auth) + >>> for installation in gi.get_installations(): + ... installation.id + '1234567' + +App installation authentication +------------------------------- + +A specific installation of a Github App can use the Github API like a normal user. +It authenticates by the Github App authentication (see above) and the installation id. +The ``AppInstallationAuth`` fetches an access token for the installation and handles its +expiration timeout. The access token is refreshed automatically. + +.. code-block:: python + + >>> auth = Auth.AppAuth(123456, private_key).get_installation_auth(installation_id, token_permissions) + >>> g = Github(auth=auth) + >>> g.get_repo("user/repo").name + 'repo' diff --git a/doc/introduction.rst b/doc/introduction.rst index cc8fa70e..59fed951 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -14,12 +14,18 @@ please `open an issue `__. First create a Github instance:: from github import Github - + + # Authentication is defined via github.Auth + from github import Auth + # using an access token - g = Github("access_token") + auth = Auth.Token("access_token") + + # Public Web Github + g = Github(auth=auth) # Github Enterprise with custom hostname - g = Github(base_url="https://{hostname}/api/v3", login_or_token="access_token") + g = Github(base_url="https://{hostname}/api/v3", auth=auth) Then play with your Github objects:: diff --git a/github/AppAuthentication.py b/github/AppAuthentication.py index 94c4b9b1..1504a8a2 100644 --- a/github/AppAuthentication.py +++ b/github/AppAuthentication.py @@ -23,13 +23,13 @@ from typing import Dict, Optional, Union +import deprecated -class AppAuthentication: - app_id: Union[int, str] - private_key: str - installation_id: int - token_permissions: Optional[Dict[str, str]] +from github.Auth import AppAuth, AppInstallationAuth + +@deprecated.deprecated("Use app.Auth.AppInstallationAuth instead") +class AppAuthentication(AppInstallationAuth): def __init__( self, app_id: Union[int, str], @@ -37,13 +37,8 @@ class AppAuthentication: installation_id: int, token_permissions: Optional[Dict[str, str]] = 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 + super().__init__( + app_auth=AppAuth(app_id, private_key), + installation_id=installation_id, + token_permissions=token_permissions, + ) diff --git a/github/ApplicationOAuth.py b/github/ApplicationOAuth.py index 997e9d17..07af42b8 100644 --- a/github/ApplicationOAuth.py +++ b/github/ApplicationOAuth.py @@ -32,6 +32,11 @@ class ApplicationOAuth(github.GithubObject.NonCompletableGithubObject): The reference can be found at https://docs.github.com/en/developers/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps """ + def __init__(self, requester, headers, attributes, completed): + # this object requires a request without authentication + requester = requester.withAuth(auth=None) + super().__init__(requester, headers, attributes, completed) + def __repr__(self): return self.get__repr__({"client_id": self._client_id.value}) @@ -91,7 +96,6 @@ class ApplicationOAuth(github.GithubObject.NonCompletableGithubObject): if state is not None: post_parameters["state"] = state - self._requester._Requester__authorizationHeader = None headers, data = self._requester.requestJsonAndCheck( "POST", "https://github.com/login/oauth/access_token", diff --git a/github/ApplicationOAuth.pyi b/github/ApplicationOAuth.pyi index 5465d83e..35147312 100644 --- a/github/ApplicationOAuth.pyi +++ b/github/ApplicationOAuth.pyi @@ -2,6 +2,7 @@ from typing import Any, Dict, Optional from github.AccessToken import AccessToken from github.GithubObject import NonCompletableGithubObject +from github.Requester import Requester class ApplicationOAuth(NonCompletableGithubObject): def __repr__(self) -> str: ... diff --git a/github/Auth.py b/github/Auth.py new file mode 100644 index 00000000..a70277b2 --- /dev/null +++ b/github/Auth.py @@ -0,0 +1,342 @@ +############################ Copyrights and license ############################ +# # +# Copyright 2023 Enrico Minack # +# # +# 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 . # +# # +################################################################################ + +import abc +import base64 +import datetime +import time +from datetime import timedelta +from typing import Dict, Optional, Union + +import jwt + +from github import Consts +from github.InstallationAuthorization import InstallationAuthorization +from github.Requester import Requester, WithRequester + +# For App authentication, time remaining before token expiration to request a new one +ACCESS_TOKEN_REFRESH_THRESHOLD_SECONDS = 20 +TOKEN_REFRESH_THRESHOLD_TIMEDELTA = timedelta( + seconds=ACCESS_TOKEN_REFRESH_THRESHOLD_SECONDS +) + + +class Auth(abc.ABC): + """ + This class is the base class of all authentication methods for Requester. + """ + + @property + @abc.abstractmethod + def token_type(self) -> str: + """ + The type of the auth token as used in the HTTP Authorization header, e.g. Bearer or Basic. + :return: token type + """ + pass + + @property + @abc.abstractmethod + def token(self) -> str: + """ + The auth token as used in the HTTP Authorization header. + :return: token + """ + pass + + +class Login(Auth): + """ + This class is used to authenticate Requester with login and password. + """ + + def __init__(self, login: str, password: str): + assert isinstance(login, str) + assert len(login) > 0 + assert isinstance(password, str) + assert len(password) > 0 + + self._login = login + self._password = password + + @property + def login(self) -> str: + return self._login + + @property + def password(self) -> str: + return self._password + + @property + def token_type(self) -> str: + return "Basic" + + @property + def token(self) -> str: + return ( + base64.b64encode(f"{self.login}:{self.password}".encode()) + .decode("utf-8") + .replace("\n", "") + ) + + +class Token(Auth): + """ + This class is used to authenticate Requester with a single constant token. + """ + + def __init__(self, token: str): + assert isinstance(token, str) + assert len(token) > 0 + self._token = token + + @property + def token_type(self) -> str: + return "token" + + @property + def token(self) -> str: + return self._token + + +class JWT(Auth): + """ + This class is the base class to authenticate with a JSON Web Token (JWT). + https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app + """ + + @property + def token_type(self) -> str: + return "Bearer" + + +class AppAuth(JWT): + """ + This class is used to authenticate Requester as a GitHub App. + https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app + """ + + def __init__( + self, + app_id: Union[int, str], + private_key: str, + jwt_expiry: int = Consts.DEFAULT_JWT_EXPIRY, + jwt_issued_at: int = Consts.DEFAULT_JWT_ISSUED_AT, + jwt_algorithm: str = Consts.DEFAULT_JWT_ALGORITHM, + ): + assert isinstance(app_id, (int, str)), app_id + if isinstance(app_id, str): + assert len(app_id) > 0, "app_id must not be empty" + assert isinstance(private_key, str) + assert len(private_key) > 0, "private_key must not be empty" + assert isinstance(jwt_expiry, int), jwt_expiry + assert Consts.MIN_JWT_EXPIRY <= jwt_expiry <= Consts.MAX_JWT_EXPIRY, jwt_expiry + + self._app_id = app_id + self._private_key = private_key + self._jwt_expiry = jwt_expiry + self._jwt_issued_at = jwt_issued_at + self._jwt_algorithm = jwt_algorithm + + @property + def app_id(self) -> Union[int, str]: + return self._app_id + + @property + def private_key(self) -> str: + return self._private_key + + @property + def token(self) -> str: + return self.create_jwt() + + def get_installation_auth( + self, + installation_id: int, + token_permissions: Optional[Dict[str, str]] = None, + requester: Optional[Requester] = None, + ) -> "AppInstallationAuth": + """ + Creates a github.Auth.AppInstallationAuth instance for an installation. + :param installation_id: installation id + :param token_permissions: optional permissions + :param requester: optional requester with app authentication + :return: + """ + return AppInstallationAuth(self, installation_id, token_permissions, requester) + + def create_jwt(self, expiration=None) -> str: + """ + 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: jwt + """ + if expiration is not None: + assert isinstance(expiration, int), expiration + assert ( + Consts.MIN_JWT_EXPIRY <= expiration <= Consts.MAX_JWT_EXPIRY + ), expiration + + now = int(time.time()) + payload = { + "iat": now + self._jwt_issued_at, + "exp": now + (expiration if expiration is not None else self._jwt_expiry), + "iss": self._app_id, + } + encrypted = jwt.encode( + payload, key=self.private_key, algorithm=self._jwt_algorithm + ) + + if isinstance(encrypted, bytes): + return encrypted.decode("utf-8") + return encrypted + + +class AppAuthToken(JWT): + """ + This class is used to authenticate Requester as a GitHub App with a single constant JWT. + https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app + """ + + def __init__(self, token: str): + assert isinstance(token, str) + assert len(token) > 0 + self._token = token + + @property + def token(self) -> str: + return self._token + + +class AppInstallationAuth(Auth, WithRequester["AppInstallationAuth"]): + """ + This class is used to authenticate Requester as a GitHub App Installation. + https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation + """ + + # imported here to avoid circular import, needed for typing only + from github.GithubIntegration import GithubIntegration + + # used to fetch live access token when calling self.token + __integration: Optional[GithubIntegration] = None + __installation_authorization: Optional[InstallationAuthorization] = None + + def __init__( + self, + app_auth: AppAuth, + installation_id: int, + token_permissions: Optional[Dict[str, str]] = None, + requester: Optional[Requester] = None, + ): + super().__init__() + + assert isinstance(app_auth, AppAuth), app_auth + assert isinstance(installation_id, int), installation_id + assert token_permissions is None or isinstance( + token_permissions, dict + ), token_permissions + + self._app_auth = app_auth + self._installation_id = installation_id + self._token_permissions = token_permissions + + if requester is not None: + assert isinstance(requester, Requester), requester + self._setRequester(requester) + + def withRequester(self, requester: Requester) -> "AppInstallationAuth": + assert isinstance(requester, Requester), requester + self._setRequester(requester.withAuth(self._app_auth)) + return self + + def _setRequester(self, requester: Requester): + super().withRequester(requester) + + from github.GithubIntegration import GithubIntegration + + self.__integration = GithubIntegration( + auth=self._app_auth, + base_url=requester.base_url, + ) + + @property + def app_id(self) -> Union[int, str]: + return self._app_auth.app_id + + @property + def private_key(self) -> str: + return self._app_auth.private_key + + @property + def installation_id(self) -> int: + return self._installation_id + + @property + def token_permissions(self) -> Optional[Dict[str, str]]: + return self._token_permissions + + @property + def token_type(self) -> str: + return "token" + + @property + def token(self) -> str: + if self.__installation_authorization is None or self._is_expired: + self.__installation_authorization = self._get_installation_authorization() + return self.__installation_authorization.token + + @property + def _is_expired(self) -> bool: + assert self.__installation_authorization is not None + token_expires_at = ( + self.__installation_authorization.expires_at + - TOKEN_REFRESH_THRESHOLD_TIMEDELTA + ) + return token_expires_at < datetime.datetime.utcnow() + + def _get_installation_authorization(self) -> InstallationAuthorization: + assert ( + self.__integration is not None + ), "Method withRequester(Requester) must be called first" + return self.__integration.get_access_token( + self._installation_id, + permissions=self._token_permissions, + ) + + +class AppUserAuth(Auth): + """ + This class is used to authenticate Requester as a GitHub App Installation on behalf of a user. + https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-with-a-github-app-on-behalf-of-a-user + """ + + def __init__(self): + raise NotImplementedError + + @property + def token_type(self) -> str: + raise NotImplementedError + + @property + def token(self) -> str: + raise NotImplementedError diff --git a/github/Consts.py b/github/Consts.py index b4b7bdd0..e442ec9d 100644 --- a/github/Consts.py +++ b/github/Consts.py @@ -148,3 +148,6 @@ 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 +# https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app +# "Your JWT must be signed using the RS256 algorithm" +DEFAULT_JWT_ALGORITHM = "RS256" diff --git a/github/GithubIntegration.py b/github/GithubIntegration.py index 475a6c0f..b6bf6910 100644 --- a/github/GithubIntegration.py +++ b/github/GithubIntegration.py @@ -1,9 +1,9 @@ -import time +import warnings import deprecated -import jwt from github import Consts +from github.Auth import AppAuth from github.GithubException import GithubException from github.Installation import Installation from github.InstallationAuthorization import InstallationAuthorization @@ -16,41 +16,66 @@ class GithubIntegration: Main class to obtain tokens for a GitHub integration. """ + # v2: remove integration_id, private_key, jwt_expiry, jwt_issued_at and jwt_algorithm + # v2: move auth to the front of arguments + # v2: add * before first argument so all arguments must be named, + # allows to reorder / add new arguments / remove deprecated arguments without breaking user code def __init__( self, - integration_id, - private_key, + integration_id=None, + private_key=None, base_url=Consts.DEFAULT_BASE_URL, jwt_expiry=Consts.DEFAULT_JWT_EXPIRY, jwt_issued_at=Consts.DEFAULT_JWT_ISSUED_AT, + jwt_algorithm=Consts.DEFAULT_JWT_ALGORITHM, + auth=None, ): """ - :param integration_id: int - :param private_key: string + :param integration_id: int deprecated, use auth=github.Auth.AppAuth(...) instead + :param private_key: string deprecated, use auth=github.Auth.AppAuth(...) instead :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 + :param jwt_expiry: int deprecated, use auth=github.Auth.AppAuth(...) instead + :param jwt_issued_at: int deprecated, use auth=github.Auth.AppAuth(...) instead + :param auth: authentication method """ - assert isinstance(integration_id, (int, str)), integration_id - assert isinstance(private_key, str), "supplied private key should be a string" + if integration_id is not None: + assert isinstance(integration_id, (int, str)), integration_id + if private_key is not None: + 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 + + if ( + integration_id is not None + or private_key is not None + or jwt_expiry != Consts.DEFAULT_JWT_EXPIRY + or jwt_issued_at != Consts.DEFAULT_JWT_ISSUED_AT + or jwt_algorithm != Consts.DEFAULT_JWT_ALGORITHM + ): + warnings.warn( + "Arguments integration_id, private_key, jwt_expiry, jwt_issued_at and jwt_algorithm are deprecated, " + "please use auth=github.Auth.AppAuth(...) instead", + category=DeprecationWarning, + ) + auth = AppAuth( + integration_id, + private_key, + jwt_expiry=jwt_expiry, + jwt_issued_at=jwt_issued_at, + jwt_algorithm=jwt_algorithm, + ) + + assert auth is not None + self.auth = auth + self.__requester = Requester( - login_or_token=None, - password=None, - jwt=self.create_jwt(), - app_auth=None, + auth=auth, base_url=self.base_url, timeout=Consts.DEFAULT_TIMEOUT, user_agent="PyGithub/Python", @@ -67,9 +92,7 @@ class GithubIntegration: :return: dict """ return { - "Authorization": f"Bearer {self.create_jwt()}", "Accept": Consts.mediaTypeIntegrationPreview, - "User-Agent": "PyGithub/Python", } def _get_installed_app(self, url): @@ -90,6 +113,9 @@ class GithubIntegration: completed=True, ) + @deprecated.deprecated( + "Use github.Auth.AppAuth.token or github.Auth.AppAuth.create_jwt(expiration) instead" + ) def create_jwt(self, expiration=None): """ Create a signed JWT @@ -97,24 +123,7 @@ class GithubIntegration: :return string: """ - if expiration is not None: - assert isinstance(expiration, int), expiration - assert ( - Consts.MIN_JWT_EXPIRY <= expiration <= Consts.MAX_JWT_EXPIRY - ), expiration - - now = int(time.time()) - payload = { - "iat": now + self.jwt_issued_at, - "exp": now + (expiration if expiration is not None else 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 + return self.auth.create_jwt(expiration) def get_access_token(self, installation_id, permissions=None): """ diff --git a/github/GithubIntegration.pyi b/github/GithubIntegration.pyi index f1f202c5..513becb6 100644 --- a/github/GithubIntegration.pyi +++ b/github/GithubIntegration.pyi @@ -1,24 +1,24 @@ from typing import Union, Optional, Dict +from github.Auth import AppAuth 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 = ... + auth: AppAuth = ... base_url: str = ... - jwt_expiry: int = ... - jwt_issued_at: int = ... __requester: Requester = ... def __init__( self, - integration_id: Union[int, str], - private_key: str, + integration_id: Optional[Union[int, str]] = ..., + private_key: Optional[str] = ..., base_url: str = ..., jwt_expiry: int = ..., jwt_issued_at: int = ..., + jwt_algorithm: str = ..., + auth: Optional[AppAuth] = ..., ) -> None: ... def _get_installed_app(self, url: str) -> Installation: ... def _get_headers(self) -> Dict[str, str]: ... diff --git a/github/GithubObject.pyi b/github/GithubObject.pyi index 28baa0bc..7dc0f27b 100644 --- a/github/GithubObject.pyi +++ b/github/GithubObject.pyi @@ -9,6 +9,7 @@ from github.PullRequestReview import PullRequestReview from github.Requester import Requester class GithubObject: + _requester: Optional[Requester] def __init__( self, requester: Optional[Requester], diff --git a/github/MainClass.py b/github/MainClass.py index 5b438d6c..0540e369 100644 --- a/github/MainClass.py +++ b/github/MainClass.py @@ -49,6 +49,7 @@ import datetime import pickle +import warnings import urllib3 @@ -60,6 +61,7 @@ import github.License import github.NamedUser import github.PaginatedList import github.Topic +from github import Auth from . import ( AuthenticatedUser, @@ -78,6 +80,10 @@ class Github: This is the main class you instantiate to access the Github API v3. Optional parameters allow different authentication methods. """ + # v2: remove login_or_token, password, jwt and app_auth + # v2: move auth to the front of arguments + # v2: add * before first argument so all arguments must be named, + # allows to reorder / add new arguments / remove deprecated arguments without breaking user code def __init__( self, login_or_token=None, @@ -91,12 +97,13 @@ class Github: verify=True, retry=None, pool_size=None, + auth=None, ): """ - :param login_or_token: string - :param password: string - :param jwt: string - :param app_auth: github.AppAuthentication + :param login_or_token: string deprecated, use auth=github.Auth.Login(...) or auth=github.Auth.Token(...) instead + :param password: string deprecated, use auth=github.Auth.Login(...) instead + :param jwt: string deprecated, use auth=github.Auth.AppAuthToken(...) instead + :param app_auth: github.AppAuthentication deprecated, use auth=github.Auth.AppInstallationAuth(...) instead :param base_url: string :param timeout: integer :param user_agent: string @@ -104,6 +111,7 @@ class Github: :param verify: boolean or string :param retry: int or urllib3.util.retry.Retry object :param pool_size: int + :param auth: authentication method """ assert login_or_token is None or isinstance(login_or_token, str), login_or_token @@ -118,12 +126,40 @@ class Github: or isinstance(retry, urllib3.util.Retry) ), retry assert pool_size is None or isinstance(pool_size, int), pool_size + assert auth is None or isinstance(auth, Auth.Auth), auth + + if password is not None: + warnings.warn( + "Arguments login_or_token and password are deprecated, please use " + "auth=github.Auth.Login(...) instead", + category=DeprecationWarning, + ) + auth = Auth.Login(login_or_token, password) + elif login_or_token is not None: + warnings.warn( + "Argument login_or_token is deprecated, please use " + "auth=github.Auth.Token(...) instead", + category=DeprecationWarning, + ) + auth = Auth.Token(login_or_token) + elif jwt is not None: + warnings.warn( + "Argument jwt is deprecated, please use " + "auth=github.Auth.AppAuth(...) or " + "auth=github.Auth.AppAuthToken(...) instead", + category=DeprecationWarning, + ) + auth = Auth.AppAuthToken(jwt) + elif app_auth is not None: + warnings.warn( + "Argument app_auth is deprecated, please use " + "auth=github.Auth.AppInstallationAuth(...) instead", + category=DeprecationWarning, + ) + auth = app_auth self.__requester = Requester( - login_or_token, - password, - jwt, - app_auth, + auth, base_url, timeout, user_agent, diff --git a/github/MainClass.pyi b/github/MainClass.pyi index 63ccf786..0a948dc1 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.Auth import Auth from github.AppAuthentication import AppAuthentication from github.AuthenticatedUser import AuthenticatedUser from github.Commit import Commit @@ -39,6 +40,7 @@ class Github: verify: bool = ..., retry: Optional[Union[int, Retry]] = ..., pool_size: Optional[int] = ..., + auth: Optional[Auth] = ..., ) -> None: ... @property def FIX_REPO_GET_GIT_REF(self) -> bool: ... diff --git a/github/Requester.py b/github/Requester.py index 2d90599b..182346a4 100644 --- a/github/Requester.py +++ b/github/Requester.py @@ -50,8 +50,6 @@ # # ################################################################################ -import base64 -import datetime import json import logging import mimetypes @@ -60,13 +58,11 @@ import re import time import urllib from io import IOBase +from typing import Generic, Optional, TypeVar import requests -from . import Consts, GithubException, GithubIntegration - -# For App authentication, time remaining before token expiration to request a new one -ACCESS_TOKEN_REFRESH_THRESHOLD_SECONDS = 20 +from . import Consts, GithubException class RequestsResponse: @@ -295,10 +291,7 @@ class Requester: def __init__( self, - login_or_token, - password, - jwt, - app_auth, + auth, base_url, timeout, user_agent, @@ -309,28 +302,9 @@ class Requester: ): self._initializeDebugFeature() - self.__installation_authorization = None - self.__app_auth = app_auth + self.__auth = auth self.__base_url = base_url - if password is not None: - login = login_or_token - b64 = ( - base64.b64encode((f"{login}:{password}").encode()) - .decode("utf-8") - .replace("\n", "") - ) - self.__authorizationHeader = f"Basic {b64}" - elif login_or_token is not None: - token = login_or_token - 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 - o = urllib.parse.urlparse(base_url) self.__hostname = o.hostname self.__port = o.port @@ -359,40 +333,32 @@ 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) - ) + self.__installation_authorization = None - 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, + # provide auth implementations that require a requester with this requester + if isinstance(self.__auth, WithRequester): + self.__auth.withRequester(self) + + @property + def base_url(self): + return self.__base_url + + def withAuth(self, auth): + """ + Create a new requester instance with identical configuration but the given authentication method. + :param auth: authentication method + :return: new Reqester implementation + """ + return Requester( + auth=auth, base_url=self.__base_url, + timeout=self.__timeout, + user_agent=self.__userAgent, + per_page=self.per_page, + verify=self.__verify, + retry=self.__retry, + pool_size=self.__pool_size, ) - 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 - if self._must_refresh_token(): - self._logger.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( @@ -558,7 +524,10 @@ class Requester: if requestHeaders is None: requestHeaders = dict() - self.__authenticate(url, requestHeaders, parameters) + if self.__auth is not None: + requestHeaders[ + "Authorization" + ] = f"{self.__auth.token_type} {self.__auth.token}" requestHeaders["User-Agent"] = self.__userAgent url = self.__makeAbsoluteUrl(url) @@ -645,11 +614,6 @@ 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 - def __makeAbsoluteUrl(self, url): # URLs generated locally will be relative to __base_url # URLs returned from the server will start with __base_url @@ -728,3 +692,23 @@ class Requester: responseHeaders, output, ) + + +T = TypeVar("T") + + +class WithRequester(Generic[T]): + """ + Mixin class that allows to set a requester. + """ + + def __init__(self): + self.__requester: Optional[Requester] = None + + @property + def requester(self) -> Requester: + return self.__requester + + def withRequester(self, requester: Requester) -> T: + self.__requester = requester + return self diff --git a/github/Requester.pyi b/github/Requester.pyi index 7c7f0877..cc38a8c3 100644 --- a/github/Requester.pyi +++ b/github/Requester.pyi @@ -1,16 +1,25 @@ from collections import OrderedDict from io import BufferedReader from logging import Logger -from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + Iterator, + Optional, + Tuple, + Union, + Generic, + TypeVar, +) from requests.models import Response +from urllib3.util import Retry -from github.AppAuthentication import AppAuthentication +from github.Auth import Auth from github.GithubObject import GithubObject from github.InstallationAuthorization import InstallationAuthorization -from urllib3.util import Retry - class HTTPRequestsConnectionClass: def __init__( self, @@ -50,8 +59,8 @@ class HTTPSRequestsConnectionClass: ) -> None: ... class Requester: + __auth: Optional[Auth] = ... __installation_authorization: Optional[InstallationAuthorization] = ... - __app_auth: Optional[AppAuthentication] = ... __logger: Logger def DEBUG_ON_RESPONSE( self, statusCode: int, responseHeader: Dict[str, str], data: str @@ -121,10 +130,7 @@ class Requester: ) -> Tuple[int, Dict[str, Any], str]: ... def __init__( self, - login_or_token: Optional[str], - password: Optional[str], - jwt: Optional[str], - app_auth: Optional[AppAuthentication], + auth: Optional[Auth], base_url: str, timeout: int, user_agent: str, @@ -133,6 +139,9 @@ class Requester: retry: Optional[Union[int, Retry]], pool_size: Optional[int], ) -> None: ... + @property + def base_url(self) -> str: ... + def withAuth(self, auth: Optional[Auth]) -> Requester: ... def _initializeDebugFeature(self) -> None: ... def check_me(self, obj: GithubObject) -> None: ... def _must_refresh_token(self) -> bool: ... @@ -207,6 +216,14 @@ class Requester: @classmethod def setOnCheckMe(cls, onCheckMe: Callable) -> None: ... +T = TypeVar("T") + +class WithRequester(Generic[T]): + __requester: Optional[Requester] + @property + def requester(self) -> Requester: ... + def withRequester(self, requester: Requester) -> T: ... + class RequestsResponse: def __init__(self, r: Response) -> None: ... def getheaders(self) -> Iterator[Any]: ... diff --git a/github/__init__.py b/github/__init__.py index 3902dd90..96d3ad67 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__ = [ + "Auth", "AppAuthentication", "BadAttributeException", "BadCredentialsException", @@ -54,6 +55,7 @@ __all__ = [ import logging +from github import Auth from github.AppAuthentication import AppAuthentication from github.GithubIntegration import GithubIntegration from github.MainClass import Github diff --git a/github/__init__.pyi b/github/__init__.pyi index 96009018..5c5e5b68 100644 --- a/github/__init__.pyi +++ b/github/__init__.pyi @@ -1,3 +1,4 @@ +from github import Auth as Auth from github.AppAuthentication import AppAuthentication as AppAuthentication from github.GithubIntegration import GithubIntegration as GithubIntegration from github.MainClass import Github as Github diff --git a/tests/Authentication.py b/tests/Authentication.py index c66dd731..d44f26d1 100644 --- a/tests/Authentication.py +++ b/tests/Authentication.py @@ -25,10 +25,15 @@ # along with PyGithub. If not, see . # # # ################################################################################ +import warnings +from unittest import mock + +import jwt import github from . import Framework +from .GithubIntegration import APP_ID, PRIVATE_KEY, PUBLIC_KEY class Authentication(Framework.BasicTestCase): @@ -36,40 +41,128 @@ class Authentication(Framework.BasicTestCase): g = github.Github() self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + def assertWarning(self, warning, expected): + self.assertWarnings(warning, expected) + + def assertWarnings(self, warning, *expecteds): + self.assertEqual(len(warning.warnings), len(expecteds)) + for message, expected in zip(warning.warnings, expecteds): + self.assertIsInstance(message, warnings.WarningMessage) + self.assertIsInstance(message.message, DeprecationWarning) + self.assertEqual(message.message.args, (expected,)) + def testBasicAuthentication(self): - g = github.Github(self.login, self.password) + with self.assertWarns(DeprecationWarning) as warning: + g = github.Github(self.login.login, self.login.password) self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + self.assertWarning( + warning, + "Arguments login_or_token and password are deprecated, please use auth=github.Auth.Login(...) instead", + ) def testOAuthAuthentication(self): - g = github.Github(self.oauth_token) + with self.assertWarns(DeprecationWarning) as warning: + g = github.Github(self.oauth_token.token) self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + self.assertWarning( + warning, + "Argument login_or_token is deprecated, please use auth=github.Auth.Token(...) instead", + ) def testJWTAuthentication(self): - g = github.Github(jwt=self.jwt) + with self.assertWarns(DeprecationWarning) as warning: + g = github.Github(jwt=self.jwt.token) self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + self.assertWarning( + warning, + "Argument jwt is deprecated, please use auth=github.Auth.AppAuth(...) or " + "auth=github.Auth.AppAuthToken(...) instead", + ) + + def testAppAuthentication(self): + with self.assertWarns(DeprecationWarning) as warning: + app_auth = github.AppAuthentication( + app_id=self.app_auth.app_id, + private_key=self.app_auth.private_key, + installation_id=29782936, + ) + g = github.Github(app_auth=app_auth) + self.assertEqual(g.get_user("ammarmallik").name, "Ammar Akbar") + self.assertWarnings( + warning, + "Call to deprecated class AppAuthentication. (Use app.Auth.AppInstallationAuth instead)", + "Argument app_auth is deprecated, please use auth=github.Auth.AppInstallationAuth(...) instead", + ) + + def testLoginAuthentication(self): + # test data copied from testBasicAuthentication to test parity + g = github.Github(auth=self.login) + self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + + def testTokenAuthentication(self): + # test data copied from testOAuthAuthentication to test parity + g = github.Github(auth=self.oauth_token) + self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + + def testAppAuthTokenAuthentication(self): + # test data copied from testJWTAuthentication to test parity + g = github.Github(auth=self.app_auth) + self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + + def testAppInstallationAuthAuthentication(self): + # test data copied from testAppAuthentication to test parity + installation_auth = github.Auth.AppInstallationAuth(self.app_auth, 29782936) + g = github.Github(auth=installation_auth) + self.assertEqual(g.get_user("ammarmallik").name, "Ammar Akbar") + + def testCreateJWT(self): + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + + with mock.patch("github.Auth.time") as t: + t.time = mock.Mock(return_value=1550055331.7435968) + token = auth.create_jwt() + + payload = jwt.decode( + token, + key=PUBLIC_KEY, + algorithms=["RS256"], + options={"verify_exp": False}, + ) + self.assertDictEqual( + payload, {"iat": 1550055271, "exp": 1550055631, "iss": APP_ID} + ) + + def testCreateJWTWithExpiration(self): + auth = github.Auth.AppAuth( + APP_ID, PRIVATE_KEY, jwt_expiry=120, jwt_issued_at=-30 + ) + + with mock.patch("github.Auth.time") as t: + t.time = mock.Mock(return_value=1550055331.7435968) + token = auth.create_jwt(60) + + payload = jwt.decode( + token, + key=PUBLIC_KEY, + algorithms=["RS256"], + options={"verify_exp": False}, + ) + self.assertDictEqual( + payload, {"iat": 1550055301, "exp": 1550055391, "iss": APP_ID} + ) def testUserAgent(self): 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") + g = github.Github(auth=github.Auth.Login("fake_login", "fake_password")) with self.assertRaises(github.GithubException): g.get_user().name def testAuthorizationHeaderWithToken(self): # See special case in Framework.fixAuthorizationHeader - g = github.Github("ZmFrZV9sb2dpbjpmYWtlX3Bhc3N3b3Jk") + g = github.Github(auth=github.Auth.Token("ZmFrZV9sb2dpbjpmYWtlX3Bhc3N3b3Jk")) with self.assertRaises(github.GithubException): g.get_user().name diff --git a/tests/Enterprise.py b/tests/Enterprise.py index b0a5ee4d..fd3da302 100644 --- a/tests/Enterprise.py +++ b/tests/Enterprise.py @@ -33,9 +33,7 @@ from . import Framework # Replay data for this test case is forged, because I don't have access to a real Github Enterprise install class Enterprise(Framework.BasicTestCase): def testHttps(self): - g = github.Github( - self.login, self.password, base_url="https://my.enterprise.com" - ) + g = github.Github(auth=self.login, base_url="https://my.enterprise.com") self.assertListKeyEqual( g.get_user().get_repos(), lambda r: r.name, @@ -60,9 +58,7 @@ class Enterprise(Framework.BasicTestCase): ) def testHttp(self): - g = github.Github( - self.login, self.password, base_url="http://my.enterprise.com" - ) + g = github.Github(auth=self.login, base_url="http://my.enterprise.com") self.assertListKeyEqual( g.get_user().get_repos(), lambda r: r.name, @@ -88,16 +84,12 @@ class Enterprise(Framework.BasicTestCase): def testUnknownUrlScheme(self): with self.assertRaises(AssertionError) as raisedexp: - github.Github( - self.login, self.password, base_url="foobar://my.enterprise.com" - ) + github.Github(auth=self.login, base_url="foobar://my.enterprise.com") self.assertEqual(raisedexp.exception.args[0], "Unknown URL scheme") def testLongUrl(self): g = github.Github( - self.login, - self.password, - base_url="http://my.enterprise.com/path/to/github", + auth=self.login, base_url="http://my.enterprise.com/path/to/github" ) repos = g.get_user().get_repos() self.assertListKeyEqual( @@ -125,9 +117,7 @@ class Enterprise(Framework.BasicTestCase): self.assertEqual(repos[0].owner.name, "Vincent Jacques") def testSpecificPort(self): - g = github.Github( - self.login, self.password, base_url="http://my.enterprise.com:8080" - ) + g = github.Github(auth=self.login, base_url="http://my.enterprise.com:8080") self.assertListKeyEqual( g.get_user().get_repos(), lambda r: r.name, diff --git a/tests/Exceptions.py b/tests/Exceptions.py index 25ec0dcc..8459dc05 100644 --- a/tests/Exceptions.py +++ b/tests/Exceptions.py @@ -84,7 +84,9 @@ class Exceptions(Framework.TestCase): def testBadAuthentication(self): with self.assertRaises(github.GithubException) as raisedexp: - github.Github("BadUser", "BadPassword").get_user().login + github.Github( + auth=github.Auth.Login("BadUser", "BadPassword") + ).get_user().login self.assertEqual(raisedexp.exception.status, 401) self.assertEqual(raisedexp.exception.data, {"message": "Bad credentials"}) self.assertEqual(str(raisedexp.exception), '401 {"message": "Bad credentials"}') @@ -102,13 +104,17 @@ class SpecificExceptions(Framework.TestCase): def testBadCredentials(self): self.assertRaises( github.BadCredentialsException, - lambda: github.Github("BadUser", "BadPassword").get_user().login, + lambda: github.Github(auth=github.Auth.Login("BadUser", "BadPassword")) + .get_user() + .login, ) def test2FARequired(self): self.assertRaises( github.TwoFactorException, - lambda: github.Github("2fauser", "password").get_user().login, + lambda: github.Github(auth=github.Auth.Login("2fauser", "password")) + .get_user() + .login, ) def testUnknownObject(self): @@ -119,9 +125,7 @@ class SpecificExceptions(Framework.TestCase): def testBadUserAgent(self): self.assertRaises( github.BadUserAgentException, - lambda: github.Github(self.login, self.password, user_agent="") - .get_user() - .name, + lambda: github.Github(auth=self.login, user_agent="").get_user().name, ) def testRateLimitExceeded(self): diff --git a/tests/Framework.py b/tests/Framework.py index 9341c634..37578885 100644 --- a/tests/Framework.py +++ b/tests/Framework.py @@ -283,12 +283,28 @@ class BasicTestCase(unittest.TestCase): ) import GithubCredentials # type: ignore - self.login = GithubCredentials.login - 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 + self.login = ( + github.Auth.Login(GithubCredentials.login, GithubCredentials.password) + if GithubCredentials.login and GithubCredentials.password + else None + ) + self.oauth_token = ( + github.Auth.Token(GithubCredentials.oauth_token) + if GithubCredentials.oauth_token + else None + ) + self.jwt = ( + github.Auth.AppAuthToken(GithubCredentials.jwt) + if GithubCredentials.jwt + else None + ) + self.app_auth = ( + github.Auth.AppAuth( + GithubCredentials.app_id, GithubCredentials.app_private_key + ) + if GithubCredentials.app_id and GithubCredentials.app_private_key + else None + ) else: github.Requester.Requester.injectConnectionClasses( lambda ignored, *args, **kwds: ReplayingHttpConnection( @@ -298,12 +314,10 @@ class BasicTestCase(unittest.TestCase): self.__openFile("r"), *args, **kwds ), ) - self.login = "login" - self.password = "password" - self.oauth_token = "oauth_token" - self.jwt = "jwt" - self.app_id = 123456 - self.app_private_key = APP_PRIVATE_KEY + self.login = github.Auth.Login("login", "password") + self.oauth_token = github.Auth.Token("oauth_token") + self.jwt = github.Auth.AppAuthToken("jwt") + self.app_auth = github.Auth.AppAuth(123456, APP_PRIVATE_KEY) httpretty.enable(allow_net_connect=False) @@ -372,15 +386,15 @@ class TestCase(BasicTestCase): if self.tokenAuthMode: self.g = github.Github( - self.oauth_token, retry=self.retry, pool_size=self.pool_size + auth=self.oauth_token, retry=self.retry, pool_size=self.pool_size ) elif self.jwtAuthMode: self.g = github.Github( - jwt=self.jwt, retry=self.retry, pool_size=self.pool_size + auth=self.jwt, retry=self.retry, pool_size=self.pool_size ) else: self.g = github.Github( - self.login, self.password, retry=self.retry, pool_size=self.pool_size + auth=self.login, retry=self.retry, pool_size=self.pool_size ) diff --git a/tests/GithubIntegration.py b/tests/GithubIntegration.py index c93c9c01..68f57fc0 100644 --- a/tests/GithubIntegration.py +++ b/tests/GithubIntegration.py @@ -1,7 +1,6 @@ -import sys import time # NOQA +import warnings -import jwt import requests # NOQA import github @@ -43,49 +42,45 @@ class GithubIntegration(Framework.BasicTestCase): self.repo_installation_id = 30614431 self.user_installation_id = 30614431 - def testCreateJWT(self): - 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, - algorithms=["RS256"], - options={"verify_exp": False}, - ) - self.assertDictEqual( - payload, {"iat": 1550055271, "exp": 1550055631, "iss": APP_ID} - ) - sys.modules["time"].time = self.origin_time + def assertWarning(self, warning, expected): + self.assertWarnings(warning, expected) - def testCreateJWTWithExpiration(self): - 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, - jwt_expiry=120, - jwt_issued_at=-30, + def assertWarnings(self, warning, *expecteds): + self.assertEqual(len(warning.warnings), len(expecteds)) + for message, expected in zip(warning.warnings, expecteds): + self.assertIsInstance(message, warnings.WarningMessage) + self.assertIsInstance(message.message, DeprecationWarning) + self.assertEqual(message.message.args, (expected,)) + + def testDeprecatedAppAuth(self): + # Replay data copied from testGetInstallations to test authentication only + with self.assertWarns(DeprecationWarning) as warning: + github_integration = github.GithubIntegration( + integration_id=APP_ID, private_key=PRIVATE_KEY + ) + installations = github_integration.get_installations() + self.assertEqual(len(list(installations)), 2) + self.assertWarning( + warning, + "Arguments integration_id, private_key, jwt_expiry, jwt_issued_at and " + "jwt_algorithm are deprecated, please use auth=github.Auth.AppAuth(...) " + "instead", ) - token = github_integration.create_jwt(60) - payload = jwt.decode( - token, - key=PUBLIC_KEY, - algorithms=["RS256"], - options={"verify_exp": False}, - ) - self.assertDictEqual( - payload, {"iat": 1550055301, "exp": 1550055391, "iss": APP_ID} - ) - sys.modules["time"].time = self.origin_time + + def testAppAuth(self): + # Replay data copied from testDeprecatedAppAuth to test parity + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) + installations = github_integration.get_installations() + self.assertEqual(len(list(installations)), 2) + + def testNoneAppAuth(self): + with self.assertRaises(AssertionError): + github.GithubIntegration(auth=None) def testGetInstallations(self): - github_integration = github.GithubIntegration( - integration_id=APP_ID, private_key=PRIVATE_KEY - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) installations = github_integration.get_installations() self.assertEqual(len(list(installations)), 2) @@ -93,9 +88,8 @@ class GithubIntegration(Framework.BasicTestCase): self.assertEqual(installations[1].id, self.repo_installation_id) def testGetAccessToken(self): - github_integration = github.GithubIntegration( - integration_id=APP_ID, private_key=PRIVATE_KEY - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) # Get repo installation access token repo_installation_authorization = github_integration.get_access_token( @@ -151,25 +145,22 @@ class GithubIntegration(Framework.BasicTestCase): ) def testGetUserInstallation(self): - github_integration = github.GithubIntegration( - integration_id=APP_ID, private_key=PRIVATE_KEY - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) 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 - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) 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 - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) installation = github_integration.get_repo_installation( owner="ammarmallik", repo="test-runner" ) @@ -177,9 +168,8 @@ class GithubIntegration(Framework.BasicTestCase): self.assertEqual(installation.id, self.repo_installation_id) def testGetAppInstallation(self): - github_integration = github.GithubIntegration( - integration_id=APP_ID, private_key=PRIVATE_KEY - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) installation = github_integration.get_app_installation( installation_id=self.org_installation_id ) @@ -187,45 +177,40 @@ class GithubIntegration(Framework.BasicTestCase): self.assertEqual(installation.id, self.org_installation_id) def testGetInstallationNotFound(self): - github_integration = github.GithubIntegration( - integration_id=APP_ID, private_key=PRIVATE_KEY - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) 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 - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) 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 - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) 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 - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) 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 - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) with self.assertRaises(github.GithubException) as raisedexp: github_integration.get_access_token( self.repo_installation_id, permissions={"test-permissions": "read"} @@ -234,9 +219,8 @@ class GithubIntegration(Framework.BasicTestCase): self.assertEqual(raisedexp.exception.status, 422) def testGetAccessTokenWithInvalidData(self): - github_integration = github.GithubIntegration( - integration_id=APP_ID, private_key=PRIVATE_KEY - ) + auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) + github_integration = github.GithubIntegration(auth=auth) with self.assertRaises(github.GithubException) as raisedexp: github_integration.get_access_token( self.repo_installation_id, permissions="invalid_data" diff --git a/tests/Issue134.py b/tests/Issue134.py index 13d971c2..8a36f9b9 100644 --- a/tests/Issue134.py +++ b/tests/Issue134.py @@ -33,13 +33,13 @@ class Issue134( Framework.BasicTestCase ): # https://github.com/jacquev6/PyGithub/pull/134 def testGetAuthorizationsFailsWhenAutenticatedThroughOAuth(self): - g = github.Github(self.oauth_token) + g = github.Github(auth=self.oauth_token) with self.assertRaises(github.GithubException) as raisedexp: list(g.get_user().get_authorizations()) self.assertEqual(raisedexp.exception.status, 404) def testGetAuthorizationsSucceedsWhenAutenticatedThroughLoginPassword(self): - g = github.Github(self.login, self.password) + g = github.Github(auth=self.login) self.assertListKeyEqual( g.get_user().get_authorizations(), lambda a: a.note, @@ -47,7 +47,7 @@ class Issue134( ) def testGetOAuthScopesFromHeader(self): - g = github.Github(self.oauth_token) + g = github.Github(auth=self.oauth_token) self.assertEqual(g.oauth_scopes, None) g.get_user().name self.assertEqual(g.oauth_scopes, ["repo", "user", "gist"]) diff --git a/tests/Issue80.py b/tests/Issue80.py index 11fcd49f..f625df62 100644 --- a/tests/Issue80.py +++ b/tests/Issue80.py @@ -35,7 +35,7 @@ class Issue80( ): # https://github.com/jacquev6/PyGithub/issues/80 def testIgnoreHttpsFromGithubEnterprise(self): g = github.Github( - self.login, self.password, base_url="http://my.enterprise.com/some/prefix" + auth=self.login, base_url="http://my.enterprise.com/some/prefix" ) # http here org = g.get_organization("BeaverSoftware") self.assertEqual( @@ -47,8 +47,7 @@ class Issue80( def testIgnoreHttpsFromGithubEnterpriseWithPort(self): g = github.Github( - self.login, - self.password, + auth=self.login, base_url="http://my.enterprise.com:1234/some/prefix", ) # http here org = g.get_organization("BeaverSoftware") diff --git a/tests/Logging_.py b/tests/Logging_.py index d8e4a519..d21c143a 100644 --- a/tests/Logging_.py +++ b/tests/Logging_.py @@ -87,7 +87,7 @@ class Logging(Framework.BasicTestCase): def testLoggingWithBasicAuthentication(self): self.assertEqual( - github.Github(self.login, self.password).get_user().name, "Vincent Jacques" + github.Github(auth=self.login).get_user().name, "Vincent Jacques" ) url = "https://api.github.com/user" requestHeaders = { @@ -116,7 +116,7 @@ class Logging(Framework.BasicTestCase): def testLoggingWithOAuthAuthentication(self): self.assertEqual( - github.Github(self.oauth_token).get_user().name, "Vincent Jacques" + github.Github(auth=self.oauth_token).get_user().name, "Vincent Jacques" ) url = "https://api.github.com/user" requestHeaders = { diff --git a/tests/PoolSize.py b/tests/PoolSize.py index f58b5077..cd42abc1 100644 --- a/tests/PoolSize.py +++ b/tests/PoolSize.py @@ -17,8 +17,7 @@ class PoolSize(Framework.TestCase): def testReturnsRepoAfterSettingPoolSizeHttp(self): g = github.Github( - self.login, - self.password, + auth=self.login, base_url="http://my.enterprise.com", pool_size=20, ) diff --git a/tests/ReplayData/Authentication.testAppAuthTokenAuthentication.txt b/tests/ReplayData/Authentication.testAppAuthTokenAuthentication.txt new file mode 100644 index 00000000..0f13b128 --- /dev/null +++ b/tests/ReplayData/Authentication.testAppAuthTokenAuthentication.txt @@ -0,0 +1,11 @@ +https +GET +api.github.com +None +/users/jacquev6 +{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '4994'), ('content-length', '623'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"0e3990a84c08ccd728a27dbe549d4f86"'), ('date', 'Sat, 26 May 2012 09:34:29 GMT'), ('x-oauth-scopes', ''), ('content-type', 'application/json; charset=utf-8'), ('x-accepted-oauth-scopes', 'user')] +{"type":"User","company":"Criteo","location":"Paris, France","hireable":false,"gravatar_id":"b68de5ae38616c296fa345d2b9df2225","bio":"","following":24,"blog":"http://vincent-jacques.net","avatar_url":"https://secure.gravatar.com/avatar/b68de5ae38616c296fa345d2b9df2225?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","followers":13,"html_url":"https://github.com/jacquev6","url":"https://api.github.com/users/jacquev6","name":"Vincent Jacques","login":"jacquev6","public_repos":11,"public_gists":1,"email":"vincent@vincent-jacques.net","id":327146,"created_at":"2010-07-09T06:10:06Z"} + diff --git a/tests/ReplayData/Authentication.testAppInstallationAuthAuthentication.txt b/tests/ReplayData/Authentication.testAppInstallationAuthAuthentication.txt new file mode 100644 index 00000000..9f2a6b2e --- /dev/null +++ b/tests/ReplayData/Authentication.testAppInstallationAuthAuthentication.txt @@ -0,0 +1,21 @@ +https +POST +api.github.com +None +/app/installations/29782936/access_tokens +{'Authorization': 'Bearer jwt_removed', 'Accept': 'application/vnd.github.machine-man-preview+json', '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/Authentication.testLoginAuthentication.txt b/tests/ReplayData/Authentication.testLoginAuthentication.txt new file mode 100644 index 00000000..4357e445 --- /dev/null +++ b/tests/ReplayData/Authentication.testLoginAuthentication.txt @@ -0,0 +1,11 @@ +https +GET +api.github.com +None +/users/jacquev6 +{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '4995'), ('content-length', '801'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"f1a68180387d296f308d6a01917e1799"'), ('date', 'Sat, 26 May 2012 09:34:28 GMT'), ('content-type', 'application/json; charset=utf-8')] +{"type":"User","disk_usage":16852,"public_repos":11,"url":"https://api.github.com/users/jacquev6","hireable":false,"plan":{"private_repos":5,"collaborators":1,"name":"micro","space":614400},"public_gists":1,"bio":"","avatar_url":"https://secure.gravatar.com/avatar/b68de5ae38616c296fa345d2b9df2225?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","private_gists":5,"collaborators":0,"company":"Criteo","gravatar_id":"b68de5ae38616c296fa345d2b9df2225","login":"jacquev6","total_private_repos":5,"blog":"http://vincent-jacques.net","email":"vincent@vincent-jacques.net","followers":13,"name":"Vincent Jacques","owned_private_repos":5,"created_at":"2010-07-09T06:10:06Z","location":"Paris, France","id":327146,"following":24,"html_url":"https://github.com/jacquev6"} + diff --git a/tests/ReplayData/Authentication.testTokenAuthentication.txt b/tests/ReplayData/Authentication.testTokenAuthentication.txt new file mode 100644 index 00000000..f6a50faf --- /dev/null +++ b/tests/ReplayData/Authentication.testTokenAuthentication.txt @@ -0,0 +1,11 @@ +https +GET +api.github.com +None +/users/jacquev6 +{'Authorization': 'token private_token_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '4994'), ('content-length', '623'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"0e3990a84c08ccd728a27dbe549d4f86"'), ('date', 'Sat, 26 May 2012 09:34:29 GMT'), ('x-oauth-scopes', ''), ('content-type', 'application/json; charset=utf-8'), ('x-accepted-oauth-scopes', 'user')] +{"type":"User","company":"Criteo","location":"Paris, France","hireable":false,"gravatar_id":"b68de5ae38616c296fa345d2b9df2225","bio":"","following":24,"blog":"http://vincent-jacques.net","avatar_url":"https://secure.gravatar.com/avatar/b68de5ae38616c296fa345d2b9df2225?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","followers":13,"html_url":"https://github.com/jacquev6","url":"https://api.github.com/users/jacquev6","name":"Vincent Jacques","login":"jacquev6","public_repos":11,"public_gists":1,"email":"vincent@vincent-jacques.net","id":327146,"created_at":"2010-07-09T06:10:06Z"} + diff --git a/tests/ReplayData/GithubIntegration.testAppAuth.txt b/tests/ReplayData/GithubIntegration.testAppAuth.txt new file mode 100644 index 00000000..762608ab --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testAppAuth.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.testDeprecatedAppAuth.txt b/tests/ReplayData/GithubIntegration.testDeprecatedAppAuth.txt new file mode 100644 index 00000000..762608ab --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testDeprecatedAppAuth.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/Retry.py b/tests/Retry.py index 19ecf49c..45cedee7 100644 --- a/tests/Retry.py +++ b/tests/Retry.py @@ -78,8 +78,7 @@ class Retry(Framework.TestCase): def testReturnsRepoAfterSettingRetryHttp(self): g = github.Github( - self.login, - self.password, + auth=self.login, base_url="http://my.enterprise.com", retry=0, ) # http here diff --git a/tox.ini b/tox.ini index f4346521..51494b42 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ commands = pytest --cov=github --cov-report=xml {posargs} basepython = python3.8 skip_install = true deps = + types-deprecated types-jwt types-requests pre-commit