Skip to content

Commit dc17dfc

Browse files
ohmayrgcf-owl-bot[bot]clundin25
authoredJul 17, 2024··
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 <clundin@google.com>
1 parent 036dac4 commit dc17dfc

File tree

5 files changed

+308
-2
lines changed

5 files changed

+308
-2
lines changed
 

‎google/auth/_credentials_base.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ class _BaseCredentials(metaclass=abc.ABCMeta):
3737
keys, scopes, and other options. These options are not changeable after
3838
construction. Some classes will provide mechanisms to copy the credentials
3939
with modifications such as :meth:`ScopedCredentials.with_scopes`.
40+
41+
Attributes:
42+
token (Optional[str]): The bearer token that can be used in HTTP headers to make
43+
authenticated requests.
4044
"""
4145

4246
def __init__(self):
4347
self.token = None
44-
"""str: The bearer token that can be used in HTTP headers to make
45-
authenticated requests."""
4648

4749
@abc.abstractmethod
4850
def refresh(self, request):

‎google/auth/aio/__init__.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Google Auth AIO Library for Python."""
16+
17+
import logging
18+
19+
from google.auth import version as google_auth_version
20+
21+
22+
__version__ = google_auth_version.__version__
23+
24+
# Set default logging handler to avoid "No handler found" warnings.
25+
logging.getLogger(__name__).addHandler(logging.NullHandler())

‎google/auth/aio/credentials.py

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
"""Interfaces for asynchronous credentials."""
17+
18+
19+
from google.auth import _helpers
20+
from google.auth import exceptions
21+
from google.auth._credentials_base import _BaseCredentials
22+
23+
24+
class Credentials(_BaseCredentials):
25+
"""Base class for all asynchronous credentials.
26+
27+
All credentials have a :attr:`token` that is used for authentication and
28+
may also optionally set an :attr:`expiry` to indicate when the token will
29+
no longer be valid.
30+
31+
Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
32+
Credentials can do this automatically before the first HTTP request in
33+
:meth:`before_request`.
34+
35+
Although the token and expiration will change as the credentials are
36+
:meth:`refreshed <refresh>` and used, credentials should be considered
37+
immutable. Various credentials will accept configuration such as private
38+
keys, scopes, and other options. These options are not changeable after
39+
construction. Some classes will provide mechanisms to copy the credentials
40+
with modifications such as :meth:`ScopedCredentials.with_scopes`.
41+
"""
42+
43+
def __init__(self):
44+
super(Credentials, self).__init__()
45+
46+
async def apply(self, headers, token=None):
47+
"""Apply the token to the authentication header.
48+
49+
Args:
50+
headers (Mapping): The HTTP request headers.
51+
token (Optional[str]): If specified, overrides the current access
52+
token.
53+
"""
54+
self._apply(headers, token=token)
55+
56+
async def refresh(self, request):
57+
"""Refreshes the access token.
58+
59+
Args:
60+
request (google.auth.aio.transport.Request): The object used to make
61+
HTTP requests.
62+
63+
Raises:
64+
google.auth.exceptions.RefreshError: If the credentials could
65+
not be refreshed.
66+
"""
67+
raise NotImplementedError("Refresh must be implemented")
68+
69+
async def before_request(self, request, method, url, headers):
70+
"""Performs credential-specific before request logic.
71+
72+
Refreshes the credentials if necessary, then calls :meth:`apply` to
73+
apply the token to the authentication header.
74+
75+
Args:
76+
request (google.auth.aio.transport.Request): The object used to make
77+
HTTP requests.
78+
method (str): The request's HTTP method or the RPC method being
79+
invoked.
80+
url (str): The request's URI or the RPC service's URI.
81+
headers (Mapping): The request's headers.
82+
"""
83+
await self.apply(headers)
84+
85+
86+
class StaticCredentials(Credentials):
87+
"""Asynchronous Credentials representing an immutable access token.
88+
89+
The credentials are considered immutable except the tokens which can be
90+
configured in the constructor ::
91+
92+
credentials = StaticCredentials(token="token123")
93+
94+
StaticCredentials does not support :meth `refresh` and assumes that the configured
95+
token is valid and not expired. StaticCredentials will never attempt to
96+
refresh the token.
97+
"""
98+
99+
def __init__(self, token):
100+
"""
101+
Args:
102+
token (str): The access token.
103+
"""
104+
super(StaticCredentials, self).__init__()
105+
self.token = token
106+
107+
@_helpers.copy_docstring(Credentials)
108+
async def refresh(self, request):
109+
raise exceptions.InvalidOperation("Static credentials cannot be refreshed.")
110+
111+
# Note: before_request should never try to refresh access tokens.
112+
# StaticCredentials intentionally does not support it.
113+
@_helpers.copy_docstring(Credentials)
114+
async def before_request(self, request, method, url, headers):
115+
await self.apply(headers)
116+
117+
118+
class AnonymousCredentials(Credentials):
119+
"""Asynchronous Credentials that do not provide any authentication information.
120+
121+
These are useful in the case of services that support anonymous access or
122+
local service emulators that do not use credentials.
123+
"""
124+
125+
async def refresh(self, request):
126+
"""Raises :class:``InvalidOperation``, anonymous credentials cannot be
127+
refreshed."""
128+
raise exceptions.InvalidOperation("Anonymous credentials cannot be refreshed.")
129+
130+
async def apply(self, headers, token=None):
131+
"""Anonymous credentials do nothing to the request.
132+
133+
The optional ``token`` argument is not supported.
134+
135+
Raises:
136+
google.auth.exceptions.InvalidValue: If a token was specified.
137+
"""
138+
if token is not None:
139+
raise exceptions.InvalidValue("Anonymous credentials don't support tokens.")
140+
141+
async def before_request(self, request, method, url, headers):
142+
"""Anonymous credentials do nothing to the request."""
143+
pass

‎system_tests/secrets.tar.enc

0 Bytes
Binary file not shown.

‎tests/test_credentials_async.py

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest # type: ignore
16+
17+
from google.auth import exceptions
18+
from google.auth.aio import credentials
19+
20+
21+
class CredentialsImpl(credentials.Credentials):
22+
pass
23+
24+
25+
def test_credentials_constructor():
26+
credentials = CredentialsImpl()
27+
assert not credentials.token
28+
29+
30+
@pytest.mark.asyncio
31+
async def test_before_request():
32+
credentials = CredentialsImpl()
33+
request = "water"
34+
headers = {}
35+
credentials.token = "orchid"
36+
37+
# before_request should not affect the value of the token.
38+
await credentials.before_request(request, "http://example.com", "GET", headers)
39+
assert credentials.token == "orchid"
40+
assert headers["authorization"] == "Bearer orchid"
41+
assert "x-allowed-locations" not in headers
42+
43+
request = "earth"
44+
headers = {}
45+
46+
# Second call shouldn't affect token or headers.
47+
await credentials.before_request(request, "http://example.com", "GET", headers)
48+
assert credentials.token == "orchid"
49+
assert headers["authorization"] == "Bearer orchid"
50+
assert "x-allowed-locations" not in headers
51+
52+
53+
@pytest.mark.asyncio
54+
async def test_static_credentials_ctor():
55+
static_creds = credentials.StaticCredentials(token="orchid")
56+
assert static_creds.token == "orchid"
57+
58+
59+
@pytest.mark.asyncio
60+
async def test_static_credentials_apply_default():
61+
static_creds = credentials.StaticCredentials(token="earth")
62+
headers = {}
63+
64+
await static_creds.apply(headers)
65+
assert headers["authorization"] == "Bearer earth"
66+
67+
await static_creds.apply(headers, token="orchid")
68+
assert headers["authorization"] == "Bearer orchid"
69+
70+
71+
@pytest.mark.asyncio
72+
async def test_static_credentials_before_request():
73+
static_creds = credentials.StaticCredentials(token="orchid")
74+
request = "water"
75+
headers = {}
76+
77+
# before_request should not affect the value of the token.
78+
await static_creds.before_request(request, "http://example.com", "GET", headers)
79+
assert static_creds.token == "orchid"
80+
assert headers["authorization"] == "Bearer orchid"
81+
assert "x-allowed-locations" not in headers
82+
83+
request = "earth"
84+
headers = {}
85+
86+
# Second call shouldn't affect token or headers.
87+
await static_creds.before_request(request, "http://example.com", "GET", headers)
88+
assert static_creds.token == "orchid"
89+
assert headers["authorization"] == "Bearer orchid"
90+
assert "x-allowed-locations" not in headers
91+
92+
93+
@pytest.mark.asyncio
94+
async def test_static_credentials_refresh():
95+
static_creds = credentials.StaticCredentials(token="orchid")
96+
request = "earth"
97+
98+
with pytest.raises(exceptions.InvalidOperation) as exc:
99+
await static_creds.refresh(request)
100+
assert exc.match("Static credentials cannot be refreshed.")
101+
102+
103+
@pytest.mark.asyncio
104+
async def test_anonymous_credentials_ctor():
105+
anon = credentials.AnonymousCredentials()
106+
assert anon.token is None
107+
108+
109+
@pytest.mark.asyncio
110+
async def test_anonymous_credentials_refresh():
111+
anon = credentials.AnonymousCredentials()
112+
request = object()
113+
with pytest.raises(exceptions.InvalidOperation) as exc:
114+
await anon.refresh(request)
115+
assert exc.match("Anonymous credentials cannot be refreshed.")
116+
117+
118+
@pytest.mark.asyncio
119+
async def test_anonymous_credentials_apply_default():
120+
anon = credentials.AnonymousCredentials()
121+
headers = {}
122+
await anon.apply(headers)
123+
assert headers == {}
124+
with pytest.raises(ValueError):
125+
await anon.apply(headers, token="orchid")
126+
127+
128+
@pytest.mark.asyncio
129+
async def test_anonymous_credentials_before_request():
130+
anon = credentials.AnonymousCredentials()
131+
request = object()
132+
method = "GET"
133+
url = "https://example.com/api/endpoint"
134+
headers = {}
135+
await anon.before_request(request, method, url, headers)
136+
assert headers == {}

0 commit comments

Comments
 (0)
Please sign in to comment.