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 1, 2024
1 parent bad2b5d commit 163c736
Show file tree
Hide file tree
Showing 5 changed files with 458 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
Expand Up @@ -3,9 +3,19 @@
expand_id_token,
)
from ._validating_storage_adapater import ValidatingStorageAdapter
from .authorizer_factory import (
AccessTokenAuthorizerFactory,
AuthorizerFactory,
ClientCredentialsAuthorizerFactory,
RefreshTokenAuthorizerFactory,
)

__all__ = [
"IdentifiedOAuthTokenResponse",
"expand_id_token",
"ValidatingStorageAdapter",
"AuthorizerFactory",
"AccessTokenAuthorizerFactory",
"RefreshTokenAuthorizerFactory",
"ClientCredentialsAuthorizerFactory",
]
205 changes: 205 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,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,
)
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 163c736

Please sign in to comment.