-
Notifications
You must be signed in to change notification settings - Fork 311
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement async
StaticCredentials
using access tokens (#1559)
* feat: implement async oauth2 credentials * minor cleanup * inherit base credentials class in async credentials * fix whitespace * implement builder class for oauth 2.0 credentials * feat: implement async static credentials * revert implementing oauth2 credentials * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * update values used in tests * update the exception raised for static refresh * add async anonymous credentials * update docstrings * address PR comments * chore: Refresh system test creds. * fix lint issues * add test coverage * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: Carl Lundin <[email protected]>
- Loading branch information
1 parent
036dac4
commit dc17dfc
Showing
5 changed files
with
308 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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 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,25 @@ | ||
# Copyright 2024 Google LLC | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Google Auth AIO Library for Python.""" | ||
|
||
import logging | ||
|
||
from google.auth import version as google_auth_version | ||
|
||
|
||
__version__ = google_auth_version.__version__ | ||
|
||
# Set default logging handler to avoid "No handler found" warnings. | ||
logging.getLogger(__name__).addHandler(logging.NullHandler()) |
This file contains 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,143 @@ | ||
# Copyright 2024 Google LLC | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
|
||
"""Interfaces for asynchronous credentials.""" | ||
|
||
|
||
from google.auth import _helpers | ||
from google.auth import exceptions | ||
from google.auth._credentials_base import _BaseCredentials | ||
|
||
|
||
class Credentials(_BaseCredentials): | ||
"""Base class for all asynchronous credentials. | ||
All credentials have a :attr:`token` that is used for authentication and | ||
may also optionally set an :attr:`expiry` to indicate when the token will | ||
no longer be valid. | ||
Most credentials will be :attr:`invalid` until :meth:`refresh` is called. | ||
Credentials can do this automatically before the first HTTP request in | ||
:meth:`before_request`. | ||
Although the token and expiration will change as the credentials are | ||
:meth:`refreshed <refresh>` and used, credentials should be considered | ||
immutable. Various credentials will accept configuration such as private | ||
keys, scopes, and other options. These options are not changeable after | ||
construction. Some classes will provide mechanisms to copy the credentials | ||
with modifications such as :meth:`ScopedCredentials.with_scopes`. | ||
""" | ||
|
||
def __init__(self): | ||
super(Credentials, self).__init__() | ||
|
||
async def apply(self, headers, token=None): | ||
"""Apply the token to the authentication header. | ||
Args: | ||
headers (Mapping): The HTTP request headers. | ||
token (Optional[str]): If specified, overrides the current access | ||
token. | ||
""" | ||
self._apply(headers, token=token) | ||
|
||
async def refresh(self, request): | ||
"""Refreshes the access token. | ||
Args: | ||
request (google.auth.aio.transport.Request): The object used to make | ||
HTTP requests. | ||
Raises: | ||
google.auth.exceptions.RefreshError: If the credentials could | ||
not be refreshed. | ||
""" | ||
raise NotImplementedError("Refresh must be implemented") | ||
|
||
async def before_request(self, request, method, url, headers): | ||
"""Performs credential-specific before request logic. | ||
Refreshes the credentials if necessary, then calls :meth:`apply` to | ||
apply the token to the authentication header. | ||
Args: | ||
request (google.auth.aio.transport.Request): The object used to make | ||
HTTP requests. | ||
method (str): The request's HTTP method or the RPC method being | ||
invoked. | ||
url (str): The request's URI or the RPC service's URI. | ||
headers (Mapping): The request's headers. | ||
""" | ||
await self.apply(headers) | ||
|
||
|
||
class StaticCredentials(Credentials): | ||
"""Asynchronous Credentials representing an immutable access token. | ||
The credentials are considered immutable except the tokens which can be | ||
configured in the constructor :: | ||
credentials = StaticCredentials(token="token123") | ||
StaticCredentials does not support :meth `refresh` and assumes that the configured | ||
token is valid and not expired. StaticCredentials will never attempt to | ||
refresh the token. | ||
""" | ||
|
||
def __init__(self, token): | ||
""" | ||
Args: | ||
token (str): The access token. | ||
""" | ||
super(StaticCredentials, self).__init__() | ||
self.token = token | ||
|
||
@_helpers.copy_docstring(Credentials) | ||
async def refresh(self, request): | ||
raise exceptions.InvalidOperation("Static credentials cannot be refreshed.") | ||
|
||
# Note: before_request should never try to refresh access tokens. | ||
# StaticCredentials intentionally does not support it. | ||
@_helpers.copy_docstring(Credentials) | ||
async def before_request(self, request, method, url, headers): | ||
await self.apply(headers) | ||
|
||
|
||
class AnonymousCredentials(Credentials): | ||
"""Asynchronous Credentials that do not provide any authentication information. | ||
These are useful in the case of services that support anonymous access or | ||
local service emulators that do not use credentials. | ||
""" | ||
|
||
async def refresh(self, request): | ||
"""Raises :class:``InvalidOperation``, anonymous credentials cannot be | ||
refreshed.""" | ||
raise exceptions.InvalidOperation("Anonymous credentials cannot be refreshed.") | ||
|
||
async def apply(self, headers, token=None): | ||
"""Anonymous credentials do nothing to the request. | ||
The optional ``token`` argument is not supported. | ||
Raises: | ||
google.auth.exceptions.InvalidValue: If a token was specified. | ||
""" | ||
if token is not None: | ||
raise exceptions.InvalidValue("Anonymous credentials don't support tokens.") | ||
|
||
async def before_request(self, request, method, url, headers): | ||
"""Anonymous credentials do nothing to the request.""" | ||
pass |
Binary file not shown.
This file contains 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,136 @@ | ||
# Copyright 2024 Google LLC | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import pytest # type: ignore | ||
|
||
from google.auth import exceptions | ||
from google.auth.aio import credentials | ||
|
||
|
||
class CredentialsImpl(credentials.Credentials): | ||
pass | ||
|
||
|
||
def test_credentials_constructor(): | ||
credentials = CredentialsImpl() | ||
assert not credentials.token | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_before_request(): | ||
credentials = CredentialsImpl() | ||
request = "water" | ||
headers = {} | ||
credentials.token = "orchid" | ||
|
||
# before_request should not affect the value of the token. | ||
await credentials.before_request(request, "http://example.com", "GET", headers) | ||
assert credentials.token == "orchid" | ||
assert headers["authorization"] == "Bearer orchid" | ||
assert "x-allowed-locations" not in headers | ||
|
||
request = "earth" | ||
headers = {} | ||
|
||
# Second call shouldn't affect token or headers. | ||
await credentials.before_request(request, "http://example.com", "GET", headers) | ||
assert credentials.token == "orchid" | ||
assert headers["authorization"] == "Bearer orchid" | ||
assert "x-allowed-locations" not in headers | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_static_credentials_ctor(): | ||
static_creds = credentials.StaticCredentials(token="orchid") | ||
assert static_creds.token == "orchid" | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_static_credentials_apply_default(): | ||
static_creds = credentials.StaticCredentials(token="earth") | ||
headers = {} | ||
|
||
await static_creds.apply(headers) | ||
assert headers["authorization"] == "Bearer earth" | ||
|
||
await static_creds.apply(headers, token="orchid") | ||
assert headers["authorization"] == "Bearer orchid" | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_static_credentials_before_request(): | ||
static_creds = credentials.StaticCredentials(token="orchid") | ||
request = "water" | ||
headers = {} | ||
|
||
# before_request should not affect the value of the token. | ||
await static_creds.before_request(request, "http://example.com", "GET", headers) | ||
assert static_creds.token == "orchid" | ||
assert headers["authorization"] == "Bearer orchid" | ||
assert "x-allowed-locations" not in headers | ||
|
||
request = "earth" | ||
headers = {} | ||
|
||
# Second call shouldn't affect token or headers. | ||
await static_creds.before_request(request, "http://example.com", "GET", headers) | ||
assert static_creds.token == "orchid" | ||
assert headers["authorization"] == "Bearer orchid" | ||
assert "x-allowed-locations" not in headers | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_static_credentials_refresh(): | ||
static_creds = credentials.StaticCredentials(token="orchid") | ||
request = "earth" | ||
|
||
with pytest.raises(exceptions.InvalidOperation) as exc: | ||
await static_creds.refresh(request) | ||
assert exc.match("Static credentials cannot be refreshed.") | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_anonymous_credentials_ctor(): | ||
anon = credentials.AnonymousCredentials() | ||
assert anon.token is None | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_anonymous_credentials_refresh(): | ||
anon = credentials.AnonymousCredentials() | ||
request = object() | ||
with pytest.raises(exceptions.InvalidOperation) as exc: | ||
await anon.refresh(request) | ||
assert exc.match("Anonymous credentials cannot be refreshed.") | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_anonymous_credentials_apply_default(): | ||
anon = credentials.AnonymousCredentials() | ||
headers = {} | ||
await anon.apply(headers) | ||
assert headers == {} | ||
with pytest.raises(ValueError): | ||
await anon.apply(headers, token="orchid") | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_anonymous_credentials_before_request(): | ||
anon = credentials.AnonymousCredentials() | ||
request = object() | ||
method = "GET" | ||
url = "https://example.com/api/endpoint" | ||
headers = {} | ||
await anon.before_request(request, method, url, headers) | ||
assert headers == {} |