Skip to content

Commit

Permalink
add AuthorizerFactory, AccessTokenAuthorizerFactory, RefreshTokenAuth…
Browse files Browse the repository at this point in the history
…orizerFactory, and ClientCredentialsAuthorizerFactory
  • Loading branch information
aaschaer committed May 7, 2024
1 parent 63dd12b commit 7ee001d
Show file tree
Hide file tree
Showing 5 changed files with 451 additions and 0 deletions.
7 changes: 7 additions & 0 deletions changelog.d/20240430_164920_aaschaer_authorizer_factory.rst
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`)
10 changes: 10 additions & 0 deletions src/globus_sdk/experimental/globus_app/__init__.py
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 src/globus_sdk/experimental/globus_app/authorizer_factory.py
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,
)
4 changes: 4 additions & 0 deletions src/globus_sdk/experimental/globus_app/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class TokenValidationError(Exception):
pass


class MissingTokensError(Exception):
pass


class IdentityMismatchError(TokenValidationError):
def __init__(self, message: str, stored_id: UUIDLike, new_id: UUIDLike):
super().__init__(message)
Expand Down

0 comments on commit 7ee001d

Please sign in to comment.