-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add AuthorizerFactory, AccessTokenAuthorizerFactory, RefreshTokenAuth…
…orizerFactory, and ClientCredentialsAuthorizerFactory
- Loading branch information
Showing
5 changed files
with
458 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
Added | ||
~~~~~ | ||
|
||
- Added ``AuthorizerFactory``, an interface for getting a ``GlobusAuthorizer`` | ||
from a ``ValidatingStorageAdapter`` to experimental along with | ||
``AccessTokenAuthorizerFactory``, ``RefreshTokenAuthorizerFactory``, and | ||
``ClientCredentialsAuthorizerFactory`` that implement it (:pr:`972`) |
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
205 changes: 205 additions & 0 deletions
205
src/globus_sdk/experimental/globus_app/authorizer_factory.py
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,205 @@ | ||
import abc | ||
import typing as t | ||
|
||
from globus_sdk import AuthLoginClient, ConfidentialAppAuthClient | ||
from globus_sdk.authorizers import ( | ||
AccessTokenAuthorizer, | ||
ClientCredentialsAuthorizer, | ||
GlobusAuthorizer, | ||
RefreshTokenAuthorizer, | ||
) | ||
from globus_sdk.services.auth import OAuthTokenResponse | ||
|
||
from ._validating_storage_adapater import ValidatingStorageAdapter | ||
from .errors import MissingTokensError | ||
|
||
|
||
class AuthorizerFactory(metaclass=abc.ABCMeta): | ||
""" | ||
An ``AuthorizerFactory`` is an interface for getting some class of | ||
``GlobusAuthorizer`` from a ``ValidatingStorageAdapter`` that meets the | ||
authorization requirements used to initialize the ``ValidatingStorageAdapter``. | ||
An ``AuthorizerFactory`` keeps a cache of authorizer objects that are | ||
re-used until the underlying storage receives new tokens | ||
""" | ||
|
||
def __init__(self, validating_storage_adaptor: ValidatingStorageAdapter): | ||
""" | ||
:param validating_storage_adaptor: The ``ValidatingStorageAdapter`` used | ||
for defining and validating the set of authorization requirements that | ||
constructed authorizers will meet and accessing underlying token storage | ||
""" | ||
self.storage_adapter = validating_storage_adaptor | ||
self._authorizer_cache: dict[str, GlobusAuthorizer] = {} | ||
|
||
def _get_tokens_or_error(self, resource_server: str) -> dict[str, t.Any]: | ||
tokens = self.storage_adapter.get_token_data(resource_server) | ||
if tokens is None: | ||
raise MissingTokensError(f"No tokens for {resource_server}") | ||
|
||
return tokens | ||
|
||
def store(self, token_res: OAuthTokenResponse) -> None: | ||
""" | ||
Store tokens in the underlying ``ValidatingStorageAdapter`` and clear cache. | ||
This is called automatically by ``AuthorizerFactory`` subclasses that return | ||
a ``RenewingAuthorizer``. | ||
:param token_res: An ``OAuthTokenResponse`` containing tokens to be stored | ||
in the underlying ``ValidatingStorageAdapter``. | ||
""" | ||
self.storage_adapter.store(token_res) | ||
self._authorizer_cache = {} | ||
|
||
def get_authorizer(self, resource_server: str) -> GlobusAuthorizer: | ||
""" | ||
Either retrieve a cached authorizer or construct a new one if none is cached. | ||
Raises ``MissingTokensError`` if the underlying ``StorageAdapter`` does not | ||
have the needed tokens to create the authorizer. | ||
:param resource_server: The resource server the authorizer will produce | ||
authentication for | ||
""" | ||
if resource_server in self._authorizer_cache: | ||
return self._authorizer_cache[resource_server] | ||
|
||
new_authorizer = self._make_authorizer(resource_server) | ||
self._authorizer_cache[resource_server] = new_authorizer | ||
return new_authorizer | ||
|
||
@abc.abstractmethod | ||
def _make_authorizer(self, resource_server: str) -> GlobusAuthorizer: | ||
""" | ||
Construct the ``GlobusAuthorizer`` class specific to this ``AuthorizerFactory`` | ||
:param resource_server: The resource server the authorizer will produce | ||
authentication for | ||
""" | ||
|
||
|
||
class AccessTokenAuthorizerFactory(AuthorizerFactory): | ||
""" | ||
An ``AuthorizerFactory`` that constructs ``AccessTokenAuthorizer``. | ||
Note that since ``AccessTokenAuthorizer`` is static and does not provide an | ||
``on_refresh`` interface an ``AccessTokenAuthorizerFactory`` will not | ||
have its cache cleared automatically. If external logic retrieves new tokens, | ||
it should call ``AccessTokenAuthorizerFactory.on_refresh`` with the token | ||
response. | ||
""" | ||
|
||
def _make_authorizer(self, resource_server: str) -> AccessTokenAuthorizer: | ||
""" | ||
Construct an ``AccessTokenAuthorizer`` for the given resource server. | ||
Raises ``MissingTokensError`` if the underlying ``StorageAdapter`` does not | ||
have access tokens for the given resource server. | ||
:param resource_server: The resource server the authorizer will produce | ||
authentication for | ||
""" | ||
tokens = self._get_tokens_or_error(resource_server) | ||
if "access_token" not in tokens: | ||
raise MissingTokensError(f"No access_token for {resource_server}") | ||
|
||
return AccessTokenAuthorizer(tokens["access_token"]) | ||
|
||
|
||
class RefreshTokenAuthorizerFactory(AuthorizerFactory): | ||
""" | ||
An ``AuthorizerFactory`` that constructs ``RefreshTokenAuthorizer``. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
validating_storage_adaptor: ValidatingStorageAdapter, | ||
auth_login_client: AuthLoginClient, | ||
): | ||
""" | ||
:param validating_storage_adaptor: The ``ValidatingStorageAdapter`` used | ||
for defining and validating the set of authorization requirements that | ||
constructed authorizers will meet and accessing underlying token storage | ||
:auth_login_client: The ``AuthLoginCLient` used for refreshing tokens with | ||
Globus Auth | ||
""" | ||
self.auth_login_client = auth_login_client | ||
super().__init__(validating_storage_adaptor) | ||
|
||
def _make_authorizer(self, resource_server: str) -> RefreshTokenAuthorizer: | ||
""" | ||
Construct a ``RefreshTokenAuthorizer`` for the given resource server. | ||
Raises ``MissingTokensError`` if the underlying ``StorageAdapter`` does not | ||
have refresh tokens for the given resource server. | ||
:param resource_server: The resource server the authorizer will produce | ||
authentication for | ||
""" | ||
tokens = self._get_tokens_or_error(resource_server) | ||
if "refresh_token" not in tokens: | ||
raise MissingTokensError(f"No refresh_token for {resource_server}") | ||
|
||
return RefreshTokenAuthorizer( | ||
refresh_token=tokens["refresh_token"], | ||
auth_client=self.auth_login_client, | ||
access_token=tokens.get("access_token"), | ||
expires_at=tokens.get("expires_at_seconds"), | ||
on_refresh=self.store, | ||
) | ||
|
||
|
||
class ClientCredentialsAuthorizerFactory(AuthorizerFactory): | ||
""" | ||
An ``AuthorizerFactory`` that constructs ``ClientCredentialsAuthorizer``. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
validating_storage_adaptor: ValidatingStorageAdapter, | ||
confidential_client: ConfidentialAppAuthClient, | ||
): | ||
""" | ||
:param validating_storage_adaptor: The ``ValidatingStorageAdapter`` used | ||
for defining and validating the set of authorization requirements that | ||
constructed authorizers will meet and accessing underlying token storage | ||
:param confidential_client: The ``ConfidentialAppAuthClient`` that will | ||
get client credentials tokens from Globus Auth to act as itself | ||
""" | ||
self.confidential_client = confidential_client | ||
super().__init__(validating_storage_adaptor) | ||
|
||
def _make_authorizer( | ||
self, | ||
resource_server: str, | ||
) -> ClientCredentialsAuthorizer: | ||
""" | ||
Construct a ``ClientCredentialsAuthorizer`` for the given resource server. | ||
Does not require that tokens exist in the validating storage adapter, | ||
but will use them if present. | ||
:param resource_server: The resource server the authorizer will produce | ||
authentication for. The ValidatingStorageAdapter used to create the | ||
ClientCredentialsAuthorizerFactory must have scope requirements defined | ||
for this resource server. | ||
""" | ||
tokens = self.storage_adapter.get_token_data(resource_server) or {} | ||
access_token = tokens.get("access_token") | ||
expires_at = tokens.get("expires_at_seconds") | ||
|
||
scopes = self.storage_adapter.scope_requirements.get(resource_server) | ||
if scopes is None: | ||
raise ValueError( | ||
"ValidatingStorageAdapter has no scope_requirements for " | ||
f"resource_server {resource_server}" | ||
) | ||
|
||
return ClientCredentialsAuthorizer( | ||
confidential_client=self.confidential_client, | ||
scopes=scopes, | ||
access_token=access_token, | ||
expires_at=expires_at, | ||
on_refresh=self.store, | ||
) |
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
Oops, something went wrong.