Skip to content

ADR 019: re-auth revamp #589

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion nutkit/frontend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .auth_token_manager import (
AuthTokenManager,
ExpirationBasedAuthTokenManager,
BasicAuthTokenManager,
BearerAuthTokenManager,
)
from .bookmark_manager import (
BookmarkManager,
Expand Down
150 changes: 120 additions & 30 deletions nutkit/frontend/auth_token_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,38 @@
Dict,
)

from .. import protocol
from ..backend import Backend
from ..protocol import (
AuthorizationToken,
AuthTokenAndExpiration,
)
from ..protocol import AuthTokenManager as AuthTokenManagerMessage
from ..protocol import (
AuthTokenManagerClose,
AuthTokenManagerGetAuthCompleted,
AuthTokenManagerGetAuthRequest,
AuthTokenManagerHandleSecurityExceptionCompleted,
AuthTokenManagerHandleSecurityExceptionRequest,
)
from ..protocol import BasicAuthTokenManager as BasicAuthTokenManagerMessage
from ..protocol import (
BasicAuthTokenProviderCompleted,
BasicAuthTokenProviderRequest,
)
from ..protocol import BearerAuthTokenManager as BearerAuthTokenManagerMessage
from ..protocol import (
BearerAuthTokenProviderCompleted,
BearerAuthTokenProviderRequest,
NewAuthTokenManager,
NewBasicAuthTokenManager,
NewBearerAuthTokenManager,
)

__all__ = [
"AuthTokenManager",
"BasicAuthTokenManager",
"BearerAuthTokenManager",
]


@dataclass
Expand All @@ -19,16 +49,16 @@ class AuthTokenManager:
def __init__(
self,
backend: Backend,
get_auth: Callable[[], protocol.AuthorizationToken],
on_auth_expired: Callable[[protocol.AuthorizationToken], None]
get_auth: Callable[[], AuthorizationToken],
handle_security_exception: Callable[[AuthorizationToken, str], bool]
):
self._backend = backend
self._get_auth = get_auth
self._on_auth_expired = on_auth_expired
self._handle_security_exception = handle_security_exception

req = protocol.NewAuthTokenManager()
req = NewAuthTokenManager()
res = backend.send_and_receive(req)
if not isinstance(res, protocol.AuthTokenManager):
if not isinstance(res, AuthTokenManagerMessage):
raise Exception(f"Should be AuthTokenManager but was {res}")

self._auth_token_manager = res
Expand All @@ -40,92 +70,152 @@ def id(self):

@classmethod
def process_callbacks(cls, request):
if isinstance(request, protocol.AuthTokenManagerGetAuthRequest):
if isinstance(request, AuthTokenManagerGetAuthRequest):
if request.auth_token_manager_id not in cls._registry:
raise Exception(
"Backend provided unknown Auth Token Manager "
f"id: {request.auth_token_manager_id} not found"
)
manager = cls._registry[request.auth_token_manager_id]
auth_token = manager._get_auth()
return protocol.AuthTokenManagerGetAuthCompleted(
return AuthTokenManagerGetAuthCompleted(
request.id, auth_token
)
if isinstance(request, protocol.AuthTokenManagerOnAuthExpiredRequest):
if isinstance(request, AuthTokenManagerHandleSecurityExceptionRequest):
if request.auth_token_manager_id not in cls._registry:
raise Exception(
"Backend provided unknown Auth Token Manager "
f"id: {request.auth_token_manager_id} not found"
)
manager = cls._registry[request.auth_token_manager_id]
manager._on_auth_expired(request.auth)
return protocol.AuthTokenManagerOnAuthExpiredCompleted(request.id)
handled = manager._handle_security_exception(request.auth,
request.error_code)
return AuthTokenManagerHandleSecurityExceptionCompleted(request.id,
handled)

def close(self, hooks=None):
res = self._backend.send_and_receive(
protocol.AuthTokenManagerClose(self.id),
AuthTokenManagerClose(self.id),
hooks=hooks
)
if not isinstance(res, protocol.AuthTokenManager):
if not isinstance(res, AuthTokenManagerMessage):
raise Exception(
f"Should be AuthTokenManager but was {res}"
)
del self._registry[self.id]


@dataclass
class ExpirationBasedAuthTokenManager:
_registry: ClassVar[Dict[Any, ExpirationBasedAuthTokenManager]] = {}
class BasicAuthTokenManager:
_registry: ClassVar[Dict[Any, BasicAuthTokenManager]] = {}

def __init__(
self,
backend: Backend,
callback: Callable[[], AuthorizationToken]
):
self._backend = backend
self._callback = callback

req = NewBasicAuthTokenManager()
res = backend.send_and_receive(req)
if not isinstance(res, BasicAuthTokenManagerMessage):
raise Exception(
f"Should be BasicAuthTokenManager but was {res}"
)

self._basic_auth_token_manager = res
self._registry[self._basic_auth_token_manager.id] = self

@property
def id(self):
return self._basic_auth_token_manager.id

@classmethod
def process_callbacks(cls, request):
if isinstance(request,
BasicAuthTokenProviderRequest):
if (
request.basic_auth_token_manager_id
not in cls._registry
):
raise Exception(
"Backend provided unknown BasicAuthTokenManager "
f"id: {request.basic_auth_token_manager_id} "
f"not found"
)

manager = cls._registry[
request.basic_auth_token_manager_id
]
renewable_auth_token = manager._callback()
return BasicAuthTokenProviderCompleted(
request.id, renewable_auth_token
)

def close(self, hooks=None):
res = self._backend.send_and_receive(
AuthTokenManagerClose(self.id),
hooks=hooks
)
if not isinstance(res, AuthTokenManagerMessage):
raise Exception(f"Should be AuthTokenManager but was {res}")
del self._registry[self.id]


@dataclass
class BearerAuthTokenManager:
_registry: ClassVar[Dict[Any, BearerAuthTokenManager]] = {}

def __init__(
self,
backend: Backend,
callback: Callable[[], protocol.AuthTokenAndExpiration]
callback: Callable[[], AuthTokenAndExpiration]
):
self._backend = backend
self._callback = callback

req = protocol.NewExpirationBasedAuthTokenManager()
req = NewBearerAuthTokenManager()
res = backend.send_and_receive(req)
if not isinstance(res, protocol.ExpirationBasedAuthTokenManager):
if not isinstance(res, BearerAuthTokenManagerMessage):
raise Exception(
f"Should be TemporalAuthTokenManager but was {res}"
f"Should be BearerAuthTokenManager but was {res}"
)

self._temporal_auth_token_manager = res
self._registry[self._temporal_auth_token_manager.id] = self
self._bearer_auth_token_manager = res
self._registry[self._bearer_auth_token_manager.id] = self

@property
def id(self):
return self._temporal_auth_token_manager.id
return self._bearer_auth_token_manager.id

@classmethod
def process_callbacks(cls, request):
if isinstance(request,
protocol.ExpirationBasedAuthTokenProviderRequest):
BearerAuthTokenProviderRequest):
if (
request.expiration_based_auth_token_manager_id
request.bearer_auth_token_manager_id
not in cls._registry
):
raise Exception(
"Backend provided unknown ExpirationBasedAuthTokenManager "
f"id: {request.expiration_based_auth_token_manager_id} "
"Backend provided unknown BearerAuthTokenManager "
f"id: {request.bearer_auth_token_manager_id} "
f"not found"
)

manager = cls._registry[
request.expiration_based_auth_token_manager_id
request.bearer_auth_token_manager_id
]
renewable_auth_token = manager._callback()
return protocol.ExpirationBasedAuthTokenProviderCompleted(
return BearerAuthTokenProviderCompleted(
request.id, renewable_auth_token
)

def close(self, hooks=None):
res = self._backend.send_and_receive(
protocol.AuthTokenManagerClose(self.id),
AuthTokenManagerClose(self.id),
hooks=hooks
)
if not isinstance(res, protocol.AuthTokenManager):
if not isinstance(res, AuthTokenManagerMessage):
raise Exception(f"Should be AuthTokenManager but was {res}")
del self._registry[self.id]
12 changes: 8 additions & 4 deletions nutkit/frontend/driver.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from .. import protocol
from .auth_token_manager import (
AuthTokenManager,
ExpirationBasedAuthTokenManager,
BasicAuthTokenManager,
BearerAuthTokenManager,
)
from .bookmark_manager import BookmarkManager
from .session import Session
Expand Down Expand Up @@ -29,7 +30,9 @@ def __init__(self, backend, uri, auth_token, user_agent=None,
self._auth_token = auth_token
else:
assert isinstance(
auth_token, (AuthTokenManager, ExpirationBasedAuthTokenManager)
auth_token, (AuthTokenManager,
BearerAuthTokenManager,
BasicAuthTokenManager)
)
self._auth_token_manager = auth_token
auth_token_manager_id = auth_token.id
Expand Down Expand Up @@ -73,9 +76,10 @@ def receive(self, timeout=None, hooks=None, *, allow_resolution):
)
continue
for cb_processor in (
BookmarkManager,
ExpirationBasedAuthTokenManager,
AuthTokenManager,
BasicAuthTokenManager,
BearerAuthTokenManager,
BookmarkManager,
):
cb_response = cb_processor.process_callbacks(res)
if cb_response is not None:
Expand Down
2 changes: 2 additions & 0 deletions nutkit/protocol/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class Feature(Enum):
# If there are more than records, the driver emits a warning.
# This method is supposed to always exhaust the result stream.
API_RESULT_SINGLE_OPTIONAL = "Feature:API:Result.SingleOptional"
# The driver offers a way to determine if exceptions are retryable or not.
API_RETRYABLE_EXCEPTION = "Feature:API:RetryableExceptions"
# The session configuration allows to switch the authentication context
# by supplying new credentials. This new context is only valid for the
# current session.
Expand Down
41 changes: 34 additions & 7 deletions nutkit/protocol/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,16 @@ def __init__(self, request_id, auth):
self.auth = auth


class AuthTokenManagerOnAuthExpiredCompleted:
class AuthTokenManagerHandleSecurityExceptionCompleted:
"""
Result of a completed auth token provider function call.
Result of a completed security exception handler call.

No response is expected.
"""

def __init__(self, request_id):
def __init__(self, request_id, handled):
self.requestId = request_id
self.handled = bool(handled)


class AuthTokenManagerClose:
Expand All @@ -203,20 +204,46 @@ def __init__(self, id):
self.id = id


class NewExpirationBasedAuthTokenManager:
class NewBasicAuthTokenManager:
"""
Create a new token manager for password rotation on the backend.

The manager will wrap a plain token provider function on the backend.

The backend should respond with `BasicAuthTokenManager`.
"""

def __init__(self):
pass


class BasicAuthTokenProviderCompleted:
"""
Result of a completed auth token provider function call.

No response is expected.
"""

def __init__(self, request_id, auth):
self.requestId = request_id
assert isinstance(auth, AuthorizationToken)
self.auth = auth


class NewBearerAuthTokenManager:
"""
Create a new auth temporal token manager on the backend.
Create a new manager for potentially expiring bearer tokens on the backend.

The manager will wrap a temporal token provider function on the backend.

The backend should respond with `ExpirationBasedAuthTokenManager`.
The backend should respond with `BearerAuthTokenManager`.
"""

def __init__(self):
pass


class ExpirationBasedAuthTokenProviderCompleted:
class BearerAuthTokenProviderCompleted:
"""
Result of a completed auth token provider function call.

Expand Down
Loading