From a3b073ba58bfce973b646b6b2ef5c03a5a0c2abc Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Wed, 7 Jun 2023 12:53:52 -0700 Subject: [PATCH] feat: Add public API load_credentials_from_dict to allow creating a default credential object from a dictionary. This resolves https://github.com/googleapis/google-auth-library-python/issues/1313. --- google/auth/__init__.py | 4 ++-- google/auth/_default.py | 44 +++++++++++++++++++++++++++++++++++++++++ tests/test__default.py | 14 +++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/google/auth/__init__.py b/google/auth/__init__.py index 861abe7ea..20f7bec90 100644 --- a/google/auth/__init__.py +++ b/google/auth/__init__.py @@ -17,13 +17,13 @@ import logging from google.auth import version as google_auth_version -from google.auth._default import default, load_credentials_from_file +from google.auth._default import default, load_credentials_from_file, load_credentials_from_dict __version__ = google_auth_version.__version__ -__all__ = ["default", "load_credentials_from_file"] +__all__ = ["default", "load_credentials_from_file", "load_credentials_from_dict"] # Set default logging handler to avoid "No handler found" warnings. logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/google/auth/_default.py b/google/auth/_default.py index 4effeec9e..1ae26b4eb 100644 --- a/google/auth/_default.py +++ b/google/auth/_default.py @@ -130,6 +130,50 @@ def load_credentials_from_file( ) +def load_credentials_from_dict( + info, scopes=None, default_scopes=None, quota_project_id=None, request=None +): + """Loads Google credentials from a dict. + + The credentials file must be a service account key, stored authorized + user credentials, external account credentials, or impersonated service + account credentials. + + Args: + info (Dict[str, Any]): A dict object containing the credentials + scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If + specified, the credentials will automatically be scoped if + necessary + default_scopes (Optional[Sequence[str]]): Default scopes passed by a + Google client library. Use 'scopes' for user-defined scopes. + quota_project_id (Optional[str]): The project ID used for + quota and billing. + request (Optional[google.auth.transport.Request]): An object used to make + HTTP requests. This is used to determine the associated project ID + for a workload identity pool resource (external account credentials). + If not specified, then it will use a + google.auth.transport.requests.Request client to make requests. + + Returns: + Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded + credentials and the project ID. Authorized user credentials do not + have the project ID information. External account credentials project + IDs may not always be determined. + + Raises: + google.auth.exceptions.DefaultCredentialsError: if the file is in the + wrong format or is missing. + """ + if not isinstance(info, dict): + raise exceptions.DefaultCredentialsError( + "info object was of type {} but dict type was expected.".format(type(info)) + ) + + return _load_credentials_from_info( + "dict object", info, scopes, default_scopes, quota_project_id, request + ) + + def _load_credentials_from_info( filename, info, scopes, default_scopes, quota_project_id, request ): diff --git a/tests/test__default.py b/tests/test__default.py index 36adf0d33..9eeb1cb25 100644 --- a/tests/test__default.py +++ b/tests/test__default.py @@ -188,6 +188,20 @@ def test_load_credentials_from_missing_file(): assert excinfo.match(r"not found") +def test_load_credentials_from_dict_non_dict_object(): + with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: + _default.load_credentials_from_dict("") + assert excinfo.match(r"dict type was expected") + + with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: + _default.load_credentials_from_dict(None) + assert excinfo.match(r"dict type was expected") + + with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: + _default.load_credentials_from_dict(1) + assert excinfo.match(r"dict type was expected") + + def test_load_credentials_from_file_invalid_json(tmpdir): jsonfile = tmpdir.join("invalid.json") jsonfile.write("{")