This repository was archived by the owner on Mar 26, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 80
feat: support self-signed JWT flow for service accounts #774
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
1002828
feat: support self-signed JWT flow for servie accounts
busunkim96 96074bd
fix: fix mypy and iam tests
busunkim96 a5e50d5
fix: remove py version req for lower bound checks
busunkim96 0fa39d7
fix: set fail-under to 100
busunkim96 f227371
fix: standardize scope / default scopes
busunkim96 04e0485
fix: use self.host in rest transport
busunkim96 a414ae3
fix: use host regex for comparison
busunkim96 2b5a7ab
fix: resolve mypy issues
busunkim96 0c95d0d
fix: fix test
busunkim96 38c3b18
refactor: simplify code after rebase
busunkim96 49ecf11
fix: fix rest operations client
busunkim96 b3409cf
Merge branch 'master' into self-signed-jwt
busunkim96 c85fdd4
fix: use default_host even with custom endpoints
busunkim96 b284a46
Merge branch 'master' into self-signed-jwt
busunkim96 93ffdd2
Merge branch 'master' into self-signed-jwt
bshaffer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,10 +2,12 @@ | |
|
|
||
| {% block content %} | ||
| import abc | ||
| import typing | ||
| from typing import Awaitable, Callable, Dict, Optional, Sequence, Union | ||
| import packaging.version | ||
| import pkg_resources | ||
|
|
||
| from google import auth # type: ignore | ||
| import google.api_core # type: ignore | ||
| from google.api_core import exceptions # type: ignore | ||
| from google.api_core import gapic_v1 # type: ignore | ||
| from google.api_core import retry as retries # type: ignore | ||
|
|
@@ -34,6 +36,18 @@ try: | |
| except pkg_resources.DistributionNotFound: | ||
| DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() | ||
|
|
||
| try: | ||
| # google.auth.__version__ was added in 1.26.0 | ||
| _GOOGLE_AUTH_VERSION = auth.__version__ | ||
| except AttributeError: | ||
| try: # try pkg_resources if it is available | ||
| _GOOGLE_AUTH_VERSION = pkg_resources.get_distribution("google-auth").version | ||
| except pkg_resources.DistributionNotFound: # pragma: NO COVER | ||
| _GOOGLE_AUTH_VERSION = None | ||
|
|
||
| _API_CORE_VERSION = google.api_core.__version__ | ||
|
|
||
|
|
||
| class {{ service.name }}Transport(abc.ABC): | ||
| """Abstract transport class for {{ service.name }}.""" | ||
|
|
||
|
|
@@ -43,13 +57,15 @@ class {{ service.name }}Transport(abc.ABC): | |
| {%- endfor %} | ||
| ) | ||
|
|
||
| DEFAULT_HOST: str = {% if service.host %}'{{ service.host }}'{% else %}{{ '' }}{% endif %} | ||
|
|
||
| def __init__( | ||
| self, *, | ||
| host: str{% if service.host %} = '{{ service.host }}'{% endif %}, | ||
| host: str = DEFAULT_HOST, | ||
| credentials: credentials.Credentials = None, | ||
| credentials_file: typing.Optional[str] = None, | ||
| scopes: typing.Optional[typing.Sequence[str]] = AUTH_SCOPES, | ||
| quota_project_id: typing.Optional[str] = None, | ||
| credentials_file: Optional[str] = None, | ||
| scopes: Optional[Sequence[str]] = None, | ||
| quota_project_id: Optional[str] = None, | ||
| client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, | ||
| **kwargs, | ||
| ) -> None: | ||
|
|
@@ -66,7 +82,7 @@ class {{ service.name }}Transport(abc.ABC): | |
| credentials_file (Optional[str]): A file with credentials that can | ||
| be loaded with :func:`google.auth.load_credentials_from_file`. | ||
| This argument is mutually exclusive with credentials. | ||
| scope (Optional[Sequence[str]]): A list of scopes. | ||
| scopes (Optional[Sequence[str]]): A list of scopes. | ||
| quota_project_id (Optional[str]): An optional project to use for billing | ||
| and quota. | ||
| client_info (google.api_core.gapic_v1.client_info.ClientInfo): | ||
|
|
@@ -80,6 +96,8 @@ class {{ service.name }}Transport(abc.ABC): | |
| host += ':443' | ||
| self._host = host | ||
|
|
||
| scopes_kwargs = self._get_scopes_kwargs(self._host, scopes) | ||
|
|
||
| # Save the scopes. | ||
| self._scopes = scopes or self.AUTH_SCOPES | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm understanding this correctly, Is that accurate?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep! Since the |
||
|
|
||
|
|
@@ -91,17 +109,59 @@ class {{ service.name }}Transport(abc.ABC): | |
| if credentials_file is not None: | ||
| credentials, _ = auth.load_credentials_from_file( | ||
| credentials_file, | ||
| scopes=self._scopes, | ||
| **scopes_kwargs, | ||
| quota_project_id=quota_project_id | ||
| ) | ||
|
|
||
| elif credentials is None: | ||
| credentials, _ = auth.default(scopes=self._scopes, quota_project_id=quota_project_id) | ||
| credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id) | ||
|
|
||
| # Save the credentials. | ||
| self._credentials = credentials | ||
|
|
||
|
|
||
| # TODO(busunkim): These two class methods are in the base transport | ||
| # to avoid duplicating code across the transport classes. These functions | ||
| # should be deleted once the minimum required versions of google-api-core | ||
| # and google-auth are increased. | ||
|
|
||
| # TODO: Remove this function once google-auth >= 1.25.0 is required | ||
| @classmethod | ||
| def _get_scopes_kwargs(cls, host: str, scopes: Optional[Sequence[str]]) -> Dict[str, Optional[Sequence[str]]]: | ||
| """Returns scopes kwargs to pass to google-auth methods depending on the google-auth version""" | ||
|
|
||
| scopes_kwargs = {} | ||
|
|
||
| if _GOOGLE_AUTH_VERSION and ( | ||
| packaging.version.parse(_GOOGLE_AUTH_VERSION) | ||
| >= packaging.version.parse("1.25.0") | ||
| ): | ||
| scopes_kwargs = {"scopes": scopes, "default_scopes": cls.AUTH_SCOPES} | ||
| else: | ||
| scopes_kwargs = {"scopes": scopes or cls.AUTH_SCOPES} | ||
|
|
||
| return scopes_kwargs | ||
|
|
||
| # TODO: Remove this function once google-api-core >= 1.26.0 is required | ||
| @classmethod | ||
| def _get_self_signed_jwt_kwargs(cls, host: str, scopes: Optional[Sequence[str]]) -> Dict[str, Union[Optional[Sequence[str]], str]]: | ||
| """Returns kwargs to pass to grpc_helpers.create_channel depending on the google-api-core version""" | ||
|
|
||
| self_signed_jwt_kwargs: Dict[str, Union[Optional[Sequence[str]], str]] = {} | ||
|
|
||
| if _API_CORE_VERSION and ( | ||
| packaging.version.parse(_API_CORE_VERSION) | ||
| >= packaging.version.parse("1.26.0") | ||
| ): | ||
| self_signed_jwt_kwargs["default_scopes"] = cls.AUTH_SCOPES | ||
| self_signed_jwt_kwargs["scopes"] = scopes | ||
| self_signed_jwt_kwargs["default_host"] = cls.DEFAULT_HOST | ||
| else: | ||
| self_signed_jwt_kwargs["scopes"] = scopes or cls.AUTH_SCOPES | ||
|
|
||
| return self_signed_jwt_kwargs | ||
|
|
||
|
|
||
| def _prep_wrapped_messages(self, client_info): | ||
| # Precompute the wrapped methods. | ||
| self._wrapped_methods = { | ||
|
|
@@ -138,11 +198,11 @@ class {{ service.name }}Transport(abc.ABC): | |
| {%- for method in service.methods.values() %} | ||
|
|
||
| @property | ||
| def {{ method.name|snake_case }}(self) -> typing.Callable[ | ||
| def {{ method.name|snake_case }}(self) -> Callable[ | ||
| [{{ method.input.ident }}], | ||
| typing.Union[ | ||
| Union[ | ||
| {{ method.output.ident }}, | ||
| typing.Awaitable[{{ method.output.ident }}] | ||
| Awaitable[{{ method.output.ident }}] | ||
| ]]: | ||
| raise NotImplementedError() | ||
| {%- endfor %} | ||
|
|
@@ -152,29 +212,29 @@ class {{ service.name }}Transport(abc.ABC): | |
| @property | ||
| def set_iam_policy( | ||
| self, | ||
| ) -> typing.Callable[ | ||
| ) -> Callable[ | ||
| [iam_policy.SetIamPolicyRequest], | ||
| typing.Union[policy.Policy, typing.Awaitable[policy.Policy]], | ||
| Union[policy.Policy, Awaitable[policy.Policy]], | ||
| ]: | ||
| raise NotImplementedError() | ||
|
|
||
| @property | ||
| def get_iam_policy( | ||
| self, | ||
| ) -> typing.Callable[ | ||
| ) -> Callable[ | ||
| [iam_policy.GetIamPolicyRequest], | ||
| typing.Union[policy.Policy, typing.Awaitable[policy.Policy]], | ||
| Union[policy.Policy, Awaitable[policy.Policy]], | ||
| ]: | ||
| raise NotImplementedError() | ||
|
|
||
| @property | ||
| def test_iam_permissions( | ||
| self, | ||
| ) -> typing.Callable[ | ||
| ) -> Callable[ | ||
| [iam_policy.TestIamPermissionsRequest], | ||
| typing.Union[ | ||
| Union[ | ||
| iam_policy.TestIamPermissionsResponse, | ||
| typing.Awaitable[iam_policy.TestIamPermissionsResponse], | ||
| Awaitable[iam_policy.TestIamPermissionsResponse], | ||
| ], | ||
| ]: | ||
| raise NotImplementedError() | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
|
|
||
| {% extends '_base.py.j2' %} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.