From 65dcd947a3b86294b081214ff8449e274a8d51e9 Mon Sep 17 00:00:00 2001 From: jkyle109 Date: Tue, 26 Jul 2022 16:00:09 -0400 Subject: [PATCH 1/7] feat(credentials): Add async credentials. --- firebase_admin/credentials.py | 133 ++++++++++++++++++++++++---------- 1 file changed, 96 insertions(+), 37 deletions(-) diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py index 5477e1cf7..4c1e68593 100644 --- a/firebase_admin/credentials.py +++ b/firebase_admin/credentials.py @@ -17,14 +17,22 @@ import json import pathlib -import google.auth -from google.auth.transport import requests -from google.oauth2 import credentials +from typing import Type + +import google.auth # type: ignore +from google.auth import default +from google.auth._default_async import default_async # type: ignore +from google.auth.transport import requests # type: ignore +from google.auth.transport import _aiohttp_requests as aiohttp_requests +from google.oauth2 import credentials # type: ignore +from google.oauth2 import _credentials_async as credentials_async from google.oauth2 import service_account +from google.oauth2 import _service_account_async as service_account_async -_request = requests.Request() -_scopes = [ +_request: requests.Request = requests.Request() +_request_async: aiohttp_requests.Request = aiohttp_requests.Request() +_scopes: list[str] = [ 'https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/datastore', 'https://www.googleapis.com/auth/devstorage.read_write', @@ -33,7 +41,7 @@ 'https://www.googleapis.com/auth/userinfo.email' ] -AccessTokenInfo = collections.namedtuple('AccessTokenInfo', ['access_token', 'expiry']) +AccessTokenInfo: Type[tuple] = collections.namedtuple('AccessTokenInfo', ['access_token', 'expiry']) """Data included in an OAuth2 access token. Contains the access token string and the expiry time. The expirty time is exposed as a @@ -44,8 +52,8 @@ class Base: """Provides OAuth2 access tokens for accessing Firebase services.""" - def get_access_token(self): - """Fetches a Google OAuth2 access token using this credential instance. + def get_access_token(self) -> tuple: + """Fetches a Google OAuth2 access token using the synchronous credential instance. Returns: AccessTokenInfo: An access token obtained using the credential. @@ -54,8 +62,22 @@ def get_access_token(self): google_cred.refresh(_request) return AccessTokenInfo(google_cred.token, google_cred.expiry) + async def get_access_token_async(self) -> tuple: + """Fetches a Google OAuth2 access token using the asynchronous credential instance. + + Returns: + AccessTokenInfo: An access token obtained using the credential. + """ + google_cred = self.get_credential_async() + await google_cred.refresh(_request_async) + return AccessTokenInfo(google_cred.token, google_cred.expiry) + def get_credential(self): - """Returns the Google credential instance used for authentication.""" + """Returns the Google synchronous credential instance used for authentication.""" + raise NotImplementedError + + def get_credential_async(self): + """Returns the Google asynchronous credential instance used for authentication.""" raise NotImplementedError @@ -64,8 +86,8 @@ class Certificate(Base): _CREDENTIAL_TYPE = 'service_account' - def __init__(self, cert): - """Initializes a credential from a Google service account certificate. + def __init__(self, cert: str) -> None: + """Initializes credentials from a Google service account certificate. Service account certificates can be downloaded as JSON files from the Firebase console. To instantiate a credential from a certificate file, either specify the file path or a @@ -95,44 +117,54 @@ def __init__(self, cert): try: self._g_credential = service_account.Credentials.from_service_account_info( json_data, scopes=_scopes) + self._g_credential_async = service_account_async.Credentials.from_service_account_info( + json_data, scopes=_scopes) except ValueError as error: raise ValueError('Failed to initialize a certificate credential. ' 'Caused by: "{0}"'.format(error)) @property - def project_id(self): + def project_id(self) -> str: return self._g_credential.project_id @property - def signer(self): + def signer(self) -> google.auth.crypt.Signer: return self._g_credential.signer @property - def service_account_email(self): + def service_account_email(self) -> str: return self._g_credential.service_account_email - def get_credential(self): - """Returns the underlying Google credential. + def get_credential(self) -> service_account.Credentials: + """Returns the underlying Google synchronous credential. Returns: - google.auth.credentials.Credentials: A Google Auth credential instance.""" + google.auth.credentials.Credentials: A Google Auth synchronous credential instance.""" return self._g_credential + def get_credential_async(self) -> service_account_async.Credentials: + """Returns the underlying Google asynchronous credential. + + Returns: + google.auth._credentials_async.Credentials: A Google Auth asynchronous credential + instance.""" + return self._g_credential_async class ApplicationDefault(Base): """A Google Application Default credential.""" - def __init__(self): + def __init__(self) -> None: """Creates an instance that will use Application Default credentials. - The credentials will be lazily initialized when get_credential() or - project_id() is called. See those methods for possible errors raised. + The credentials will be lazily initialized when get_credential(), get_credential_async() + or project_id() is called. See those methods for possible errors raised. """ super(ApplicationDefault, self).__init__() self._g_credential = None # Will be lazily-loaded via _load_credential(). + self._g_credential_async = None # Will be lazily-loaded via _load_credential_async(). - def get_credential(self): - """Returns the underlying Google credential. + def get_credential(self) -> credentials.Credentials: + """Returns the underlying Google synchronous credential. Raises: google.auth.exceptions.DefaultCredentialsError: If Application Default @@ -142,9 +174,20 @@ def get_credential(self): self._load_credential() return self._g_credential + def get_credential_async(self) -> credentials_async.Credentials: + """Returns the underlying Google asynchronous credential. + + Raises: + google.auth.exceptions.DefaultCredentialsError: If Application Default + credentials cannot be initialized in the current environment. + Returns: + google.auth._credentials_async.Credentials: A Google Auth credential instance.""" + self._load_credential_async() + return self._g_credential_async + @property - def project_id(self): - """Returns the project_id from the underlying Google credential. + def project_id(self) -> str: + """Returns the project_id from the underlying Google credentials. Raises: google.auth.exceptions.DefaultCredentialsError: If Application Default @@ -154,21 +197,25 @@ def project_id(self): self._load_credential() return self._project_id - def _load_credential(self): + def _load_credential(self) -> None: if not self._g_credential: - self._g_credential, self._project_id = google.auth.default(scopes=_scopes) + self._g_credential, self._project_id = default(scopes=_scopes) + + def _load_credential_async(self) -> None: + if not self._g_credential_async: + self._g_credential_async, self._project_id = default_async(scopes=_scopes) class RefreshToken(Base): - """A credential initialized from an existing refresh token.""" + """Credentials initialized from an existing refresh token.""" _CREDENTIAL_TYPE = 'authorized_user' - def __init__(self, refresh_token): - """Initializes a credential from a refresh token JSON file. + def __init__(self, refresh_token: str) -> None: + """Initializes credentials from a refresh token JSON file. The JSON must consist of client_id, client_secret and refresh_token fields. Refresh token files are typically created and managed by the gcloud SDK. To instantiate - a credential from a refresh token file, either specify the file path or a dict + credentials from a refresh token file, either specify the file path or a dict representing the parsed contents of the file. Args: @@ -194,28 +241,40 @@ def __init__(self, refresh_token): raise ValueError('Invalid refresh token configuration. JSON must contain a ' '"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE)) self._g_credential = credentials.Credentials.from_authorized_user_info(json_data, _scopes) + self._g_credential_async = credentials_async.Credentials.from_authorized_user_info( + json_data, + _scopes + ) @property - def client_id(self): + def client_id(self) -> str: return self._g_credential.client_id @property - def client_secret(self): + def client_secret(self) -> str: return self._g_credential.client_secret @property - def refresh_token(self): + def refresh_token(self) -> str: return self._g_credential.refresh_token - def get_credential(self): - """Returns the underlying Google credential. + def get_credential(self) -> credentials.Credentials: + """Returns the underlying Google synchronous credential. Returns: - google.auth.credentials.Credentials: A Google Auth credential instance.""" + google.auth.credentials.Credentials: A Google Auth synchronous credential instance.""" return self._g_credential + def get_credential_async(self) -> credentials_async.Credentials: + """Returns the underlying Google asynchronous credential. + + Returns: + google.auth._credentials_async.Credentials: A Google Auth asynchronous credential + instance.""" + return self._g_credential_async + -def _is_file_path(path): +def _is_file_path(path) -> bool: try: pathlib.Path(path) return True From 1a1a348cd49fe2c3218a2a0ce3dec5d9ffa38e76 Mon Sep 17 00:00:00 2001 From: jkyle109 Date: Tue, 26 Jul 2022 16:54:33 -0400 Subject: [PATCH 2/7] fix: Added to required modules libraries. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0dd529c04..b8aa3d724 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ pytest >= 6.2.0 pytest-cov >= 2.4.0 pytest-localserver >= 0.4.1 +aiohttp == 3.8.1 cachecontrol >= 0.12.6 google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != 'PyPy' google-api-python-client >= 1.7.8 From bb8fd246503f799a6d273e345b0db40e7fd7db1e Mon Sep 17 00:00:00 2001 From: jkyle109 Date: Tue, 26 Jul 2022 17:06:13 -0400 Subject: [PATCH 3/7] fix: Imported correct instance of typing.List. --- firebase_admin/credentials.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py index 4c1e68593..b25f4e377 100644 --- a/firebase_admin/credentials.py +++ b/firebase_admin/credentials.py @@ -17,7 +17,10 @@ import json import pathlib -from typing import Type +from typing import ( + Type, + List +) import google.auth # type: ignore from google.auth import default @@ -32,7 +35,7 @@ _request: requests.Request = requests.Request() _request_async: aiohttp_requests.Request = aiohttp_requests.Request() -_scopes: list[str] = [ +_scopes: List[str] = [ 'https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/datastore', 'https://www.googleapis.com/auth/devstorage.read_write', From bb6e93aab7169f3d674cdacd1125473cfaf8a266 Mon Sep 17 00:00:00 2001 From: jkyle109 Date: Tue, 26 Jul 2022 17:47:17 -0400 Subject: [PATCH 4/7] fix: Added method in subclass to override abstract method in class. --- integration/test_auth.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/integration/test_auth.py b/integration/test_auth.py index 82974732d..3af53e29f 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -898,13 +898,24 @@ class CredentialWrapper(credentials.Base): def __init__(self, token): self._delegate = google.oauth2.credentials.Credentials(token) + self._delegate_async = google.oauth2._credentials_async.Credentials(token) def get_credential(self): return self._delegate + def get_credential_async(self): + return self._delegate_async + @classmethod def from_existing_credential(cls, google_cred): if not google_cred.token: request = transport.requests.Request() google_cred.refresh(request) return CredentialWrapper(google_cred.token) + + @classmethod + async def from_existing_credential_async(cls, google_cred): + if not google_cred.token: + request = transport._aiohttp_requests.Request() + await google_cred.refresh(request) + return CredentialWrapper(google_cred.token) From 598351751b70dd3377d47b0947e897caecf76f96 Mon Sep 17 00:00:00 2001 From: jkyle109 Date: Wed, 3 Aug 2022 17:43:39 -0400 Subject: [PATCH 5/7] fix: Added code font for literal references. --- firebase_admin/credentials.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py index b25f4e377..27c26c8ef 100644 --- a/firebase_admin/credentials.py +++ b/firebase_admin/credentials.py @@ -159,8 +159,8 @@ class ApplicationDefault(Base): def __init__(self) -> None: """Creates an instance that will use Application Default credentials. - The credentials will be lazily initialized when get_credential(), get_credential_async() - or project_id() is called. See those methods for possible errors raised. + The credentials will be lazily initialized when ``get_credential()``, ``get_credential_async()`` + or ``project_id()`` is called. See those methods for possible errors raised. """ super(ApplicationDefault, self).__init__() self._g_credential = None # Will be lazily-loaded via _load_credential(). @@ -216,7 +216,7 @@ class RefreshToken(Base): def __init__(self, refresh_token: str) -> None: """Initializes credentials from a refresh token JSON file. - The JSON must consist of client_id, client_secret and refresh_token fields. Refresh + The JSON must consist of ``client_id``, ``client_secret`` and ``refresh_token`` fields. Refresh token files are typically created and managed by the gcloud SDK. To instantiate credentials from a refresh token file, either specify the file path or a dict representing the parsed contents of the file. From 464049f36671d70bbb0de79a9bacc7208d63b3d0 Mon Sep 17 00:00:00 2001 From: jkyle109 Date: Wed, 3 Aug 2022 17:47:46 -0400 Subject: [PATCH 6/7] fix: lint --- firebase_admin/credentials.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py index 27c26c8ef..7559a3ac4 100644 --- a/firebase_admin/credentials.py +++ b/firebase_admin/credentials.py @@ -159,8 +159,9 @@ class ApplicationDefault(Base): def __init__(self) -> None: """Creates an instance that will use Application Default credentials. - The credentials will be lazily initialized when ``get_credential()``, ``get_credential_async()`` - or ``project_id()`` is called. See those methods for possible errors raised. + The credentials will be lazily initialized when ``get_credential()``, + ``get_credential_async()`` or ``project_id()`` is called. See those methods for possible + errors raised. """ super(ApplicationDefault, self).__init__() self._g_credential = None # Will be lazily-loaded via _load_credential(). @@ -216,8 +217,8 @@ class RefreshToken(Base): def __init__(self, refresh_token: str) -> None: """Initializes credentials from a refresh token JSON file. - The JSON must consist of ``client_id``, ``client_secret`` and ``refresh_token`` fields. Refresh - token files are typically created and managed by the gcloud SDK. To instantiate + The JSON must consist of ``client_id``, ``client_secret`` and ``refresh_token`` fields. + Refresh token files are typically created and managed by the gcloud SDK. To instantiate credentials from a refresh token file, either specify the file path or a dict representing the parsed contents of the file. From d2e100c2de29d836a5b67baedf54a3be73e4891b Mon Sep 17 00:00:00 2001 From: jkyle109 Date: Thu, 4 Aug 2022 14:49:43 -0400 Subject: [PATCH 7/7] fix: update aiohttp version requirement to allow future versions. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b8aa3d724..722b774e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pytest >= 6.2.0 pytest-cov >= 2.4.0 pytest-localserver >= 0.4.1 -aiohttp == 3.8.1 +aiohttp >= 3.8.1 cachecontrol >= 0.12.6 google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != 'PyPy' google-api-python-client >= 1.7.8