Skip to content

Commit

Permalink
add AuthorizerFactory and implementations (#985)
Browse files Browse the repository at this point in the history
* add AuthorizerFactory, AccessTokenAuthorizerFactory, RefreshTokenAuthorizerFactory, and ClientCredentialsAuthorizerFactory

* Fix changelog PR number

Co-authored-by: Stephen Rosen <[email protected]>

* refactors from review

---------

Co-authored-by: Stephen Rosen <[email protected]>
  • Loading branch information
aaschaer and sirosen authored May 30, 2024
1 parent 97f37ac commit 449a5a7
Show file tree
Hide file tree
Showing 5 changed files with 465 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 ``ValidatingTokenStorage`` to experimental along with
``AccessTokenAuthorizerFactory``, ``RefreshTokenAuthorizerFactory``, and
``ClientCredentialsAuthorizerFactory`` that implement it (:pr:`985`)
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",
]
211 changes: 211 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,211 @@
from __future__ import annotations

import abc
import typing as t

from globus_sdk import AuthLoginClient, ConfidentialAppAuthClient
from globus_sdk.authorizers import (
AccessTokenAuthorizer,
ClientCredentialsAuthorizer,
GlobusAuthorizer,
RefreshTokenAuthorizer,
)
from globus_sdk.experimental.tokenstorage import TokenData
from globus_sdk.services.auth import OAuthTokenResponse

from ._validating_token_storage import ValidatingTokenStorage
from .errors import MissingTokensError

GA = t.TypeVar("GA", bound=GlobusAuthorizer)


class AuthorizerFactory(
t.Generic[GA],
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 its ``store_token_response`` method is called.
"""

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, GA] = {}

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_token_response_and_clear_cache(
self, token_res: OAuthTokenResponse
) -> None:
"""
Store a token response in the underlying ``ValidatingTokenStorage``
and clear the authorizer cache.
This should not be called when a ``RenewingAuthorizer`` created by this factory
gets new tokens for itself as there is no need to clear the cache.
:param token_res: An ``OAuthTokenResponse`` containing token data to be stored
in the underlying ``ValidatingTokenStorage``.
"""
self.token_storage.store_token_response(token_res)
self._authorizer_cache = {}

def get_authorizer(self, resource_server: str) -> GA:
"""
Either retrieve a cached authorizer for the given resource server 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) -> GA:
"""
Construct the ``GlobusAuthorizer`` class specific to this ``AuthorizerFactory``
:param resource_server: The resource server the authorizer will produce
authentication for
"""


class AccessTokenAuthorizerFactory(AuthorizerFactory[AccessTokenAuthorizer]):
"""
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[RefreshTokenAuthorizer]):
"""
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_token_response,
)


class ClientCredentialsAuthorizerFactory(
AuthorizerFactory[ClientCredentialsAuthorizer]
):
"""
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_token_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
Loading

0 comments on commit 449a5a7

Please sign in to comment.