-
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
451 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,15 @@ | ||
from ._validating_token_storage import ValidatingTokenStorage | ||
from .authorizer_factory import ( | ||
AccessTokenAuthorizerFactory, | ||
AuthorizerFactory, | ||
ClientCredentialsAuthorizerFactory, | ||
RefreshTokenAuthorizerFactory, | ||
) | ||
|
||
__all__ = [ | ||
"ValidatingTokenStorage", | ||
"AuthorizerFactory", | ||
"AccessTokenAuthorizerFactory", | ||
"RefreshTokenAuthorizerFactory", | ||
"ClientCredentialsAuthorizerFactory", | ||
] |
197 changes: 197 additions & 0 deletions
197
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,197 @@ | ||
from __future__ import annotations | ||
|
||
import abc | ||
|
||
from globus_sdk import AuthLoginClient, ConfidentialAppAuthClient | ||
from globus_sdk.authorizers import ( | ||
AccessTokenAuthorizer, | ||
ClientCredentialsAuthorizer, | ||
GlobusAuthorizer, | ||
RefreshTokenAuthorizer, | ||
) | ||
from globus_sdk.experimental.tokenstorage_v2 import TokenData | ||
from globus_sdk.services.auth import OAuthTokenResponse | ||
|
||
from ._validating_token_storage import ValidatingTokenStorage | ||
from .errors import MissingTokensError | ||
|
||
|
||
class AuthorizerFactory(metaclass=abc.ABCMeta): | ||
""" | ||
An ``AuthorizerFactory`` is an interface for getting some class of | ||
``GlobusAuthorizer`` from a ``ValidatingTokenStorage`` that meets the | ||
authorization requirements used to initialize the ``ValidatingTokenStorage``. | ||
An ``AuthorizerFactory`` keeps a cache of authorizer objects that are | ||
re-used until the underlying storage receives new tokens. | ||
""" | ||
|
||
def __init__(self, token_storage: ValidatingTokenStorage): | ||
""" | ||
:param token_storage: The ``ValidatingTokenStorage`` used | ||
for defining and validating the set of authorization requirements that | ||
constructed authorizers will meet and accessing underlying token storage | ||
""" | ||
self.token_storage = token_storage | ||
self._authorizer_cache: dict[str, GlobusAuthorizer] = {} | ||
|
||
def _get_token_data_or_error(self, resource_server: str) -> TokenData: | ||
token_data = self.token_storage.get_token_data(resource_server) | ||
if token_data is None: | ||
raise MissingTokensError(f"No token data for {resource_server}") | ||
|
||
return token_data | ||
|
||
def store_response(self, token_res: OAuthTokenResponse) -> None: | ||
""" | ||
Store a token response in the underlying ``ValidatingTokenStorage`` | ||
and clear cache. | ||
:param token_res: An ``OAuthTokenResponse`` containing token data to be stored | ||
in the underlying ``ValidatingTokenStorage``. | ||
""" | ||
self.token_storage.store_response(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 ``TokenStorage`` 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``. | ||
""" | ||
|
||
def _make_authorizer(self, resource_server: str) -> AccessTokenAuthorizer: | ||
""" | ||
Construct an ``AccessTokenAuthorizer`` for the given resource server. | ||
Raises ``MissingTokensError`` if the underlying ``TokenStorage`` does not | ||
have token data for the given resource server. | ||
:param resource_server: The resource server the authorizer will produce | ||
authentication for | ||
""" | ||
token_data = self._get_token_data_or_error(resource_server) | ||
return AccessTokenAuthorizer(token_data.access_token) | ||
|
||
|
||
class RefreshTokenAuthorizerFactory(AuthorizerFactory): | ||
""" | ||
An ``AuthorizerFactory`` that constructs ``RefreshTokenAuthorizer``. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
token_storage: ValidatingTokenStorage, | ||
auth_login_client: AuthLoginClient, | ||
): | ||
""" | ||
:param token_storage: The ``ValidatingTokenStorage`` 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__(token_storage) | ||
|
||
def _make_authorizer(self, resource_server: str) -> RefreshTokenAuthorizer: | ||
""" | ||
Construct a ``RefreshTokenAuthorizer`` for the given resource server. | ||
Raises ``MissingTokensError`` if the underlying ``TokenStorage`` does not | ||
have a refresh token for the given resource server. | ||
:param resource_server: The resource server the authorizer will produce | ||
authentication for | ||
""" | ||
token_data = self._get_token_data_or_error(resource_server) | ||
if token_data.refresh_token is None: | ||
raise MissingTokensError(f"No refresh_token for {resource_server}") | ||
|
||
return RefreshTokenAuthorizer( | ||
refresh_token=token_data.refresh_token, | ||
auth_client=self.auth_login_client, | ||
access_token=token_data.access_token, | ||
expires_at=token_data.expires_at_seconds, | ||
on_refresh=self.token_storage.store_response, | ||
) | ||
|
||
|
||
class ClientCredentialsAuthorizerFactory(AuthorizerFactory): | ||
""" | ||
An ``AuthorizerFactory`` that constructs ``ClientCredentialsAuthorizer``. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
token_storage: ValidatingTokenStorage, | ||
confidential_client: ConfidentialAppAuthClient, | ||
): | ||
""" | ||
:param token_storage: The ``ValidatingTokenStorage`` 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__(token_storage) | ||
|
||
def _make_authorizer( | ||
self, | ||
resource_server: str, | ||
) -> ClientCredentialsAuthorizer: | ||
""" | ||
Construct a ``ClientCredentialsAuthorizer`` for the given resource server. | ||
Does not require that tokens exist in the token storage but will use them if | ||
present. | ||
:param resource_server: The resource server the authorizer will produce | ||
authentication for. The ValidatingTokenStorage used to create the | ||
ClientCredentialsAuthorizerFactory must have scope requirements defined | ||
for this resource server. | ||
""" | ||
token_data = self.token_storage.get_token_data(resource_server) | ||
access_token = token_data.access_token if token_data else None | ||
expires_at = token_data.expires_at_seconds if token_data else None | ||
|
||
scopes = self.token_storage.scope_requirements.get(resource_server) | ||
if scopes is None: | ||
raise ValueError( | ||
"ValidatingTokenStorage 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.token_storage.store_response, | ||
) |
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.