xtr-security-core 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xtr_security_core/__init__.py +146 -0
- xtr_security_core/authentication/__init__.py +11 -0
- xtr_security_core/authentication/authentication_trust_resolver.py +39 -0
- xtr_security_core/authentication/authentication_trust_resolver_interface.py +30 -0
- xtr_security_core/authentication/token/__init__.py +15 -0
- xtr_security_core/authentication/token/abstract_token.py +85 -0
- xtr_security_core/authentication/token/null_token.py +23 -0
- xtr_security_core/authentication/token/storage/__init__.py +8 -0
- xtr_security_core/authentication/token/storage/token_storage.py +50 -0
- xtr_security_core/authentication/token/storage/token_storage_interface.py +29 -0
- xtr_security_core/authentication/token/token_interface.py +56 -0
- xtr_security_core/authentication/token/username_password_token.py +42 -0
- xtr_security_core/authentication_events.py +30 -0
- xtr_security_core/authorization/__init__.py +55 -0
- xtr_security_core/authorization/access_decision.py +46 -0
- xtr_security_core/authorization/access_decision_manager.py +180 -0
- xtr_security_core/authorization/access_decision_manager_interface.py +40 -0
- xtr_security_core/authorization/authorization_checker.py +86 -0
- xtr_security_core/authorization/authorization_checker_interface.py +29 -0
- xtr_security_core/authorization/guest_authorization_checker_interface.py +33 -0
- xtr_security_core/authorization/is_granted_context.py +43 -0
- xtr_security_core/authorization/strategy/__init__.py +17 -0
- xtr_security_core/authorization/strategy/access_decision_strategy_interface.py +35 -0
- xtr_security_core/authorization/strategy/affirmative_strategy.py +54 -0
- xtr_security_core/authorization/strategy/consensus_strategy.py +61 -0
- xtr_security_core/authorization/strategy/priority_strategy.py +51 -0
- xtr_security_core/authorization/strategy/unanimous_strategy.py +54 -0
- xtr_security_core/authorization/voter/__init__.py +27 -0
- xtr_security_core/authorization/voter/access.py +21 -0
- xtr_security_core/authorization/voter/authenticated_voter.py +89 -0
- xtr_security_core/authorization/voter/cacheable_voter_interface.py +31 -0
- xtr_security_core/authorization/voter/closure_voter.py +91 -0
- xtr_security_core/authorization/voter/role_hierarchy_voter.py +39 -0
- xtr_security_core/authorization/voter/role_voter.py +70 -0
- xtr_security_core/authorization/voter/traceable_voter.py +87 -0
- xtr_security_core/authorization/voter/vote.py +35 -0
- xtr_security_core/authorization/voter/voter.py +80 -0
- xtr_security_core/authorization/voter/voter_interface.py +43 -0
- xtr_security_core/event/__init__.py +9 -0
- xtr_security_core/event/authentication_event.py +32 -0
- xtr_security_core/event/authentication_success_event.py +22 -0
- xtr_security_core/event/vote_event.py +41 -0
- xtr_security_core/exception/__init__.py +48 -0
- xtr_security_core/exception/access_denied_error.py +47 -0
- xtr_security_core/exception/account_expired_error.py +15 -0
- xtr_security_core/exception/account_status_error.py +41 -0
- xtr_security_core/exception/authentication_credentials_not_found_error.py +19 -0
- xtr_security_core/exception/authentication_error.py +61 -0
- xtr_security_core/exception/authentication_service_error.py +22 -0
- xtr_security_core/exception/bad_credentials_error.py +20 -0
- xtr_security_core/exception/credentials_expired_error.py +15 -0
- xtr_security_core/exception/custom_user_message_account_status_error.py +39 -0
- xtr_security_core/exception/custom_user_message_authentication_error.py +36 -0
- xtr_security_core/exception/disabled_error.py +15 -0
- xtr_security_core/exception/insufficient_authentication_error.py +19 -0
- xtr_security_core/exception/invalid_argument_error.py +18 -0
- xtr_security_core/exception/locked_error.py +15 -0
- xtr_security_core/exception/security_error.py +14 -0
- xtr_security_core/exception/unsupported_user_error.py +15 -0
- xtr_security_core/exception/user_not_found_error.py +44 -0
- xtr_security_core/py.typed +0 -0
- xtr_security_core/role/__init__.py +8 -0
- xtr_security_core/role/role_hierarchy.py +123 -0
- xtr_security_core/role/role_hierarchy_interface.py +28 -0
- xtr_security_core/user/__init__.py +43 -0
- xtr_security_core/user/attributes_based_user_provider_interface.py +41 -0
- xtr_security_core/user/chain_user_checker.py +57 -0
- xtr_security_core/user/chain_user_provider.py +110 -0
- xtr_security_core/user/equatable_interface.py +25 -0
- xtr_security_core/user/in_memory_user.py +87 -0
- xtr_security_core/user/in_memory_user_checker.py +48 -0
- xtr_security_core/user/in_memory_user_provider.py +96 -0
- xtr_security_core/user/oidc_user.py +66 -0
- xtr_security_core/user/password_upgrader_interface.py +33 -0
- xtr_security_core/user/user_checker_interface.py +43 -0
- xtr_security_core/user/user_interface.py +38 -0
- xtr_security_core/user/user_provider_interface.py +33 -0
- xtr_security_core-3.0.0.dist-info/METADATA +339 -0
- xtr_security_core-3.0.0.dist-info/RECORD +81 -0
- xtr_security_core-3.0.0.dist-info/WHEEL +4 -0
- xtr_security_core-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The security core for xtr applications.
|
|
2
|
+
|
|
3
|
+
Users, tokens, roles, voters and the authorization decision — everything that
|
|
4
|
+
needs no HTTP edge and no JSON Web Tokens. This module gathers the public
|
|
5
|
+
surface so an application imports it from one place.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .authentication import (
|
|
11
|
+
AuthenticationTrustResolver,
|
|
12
|
+
AuthenticationTrustResolverInterface,
|
|
13
|
+
)
|
|
14
|
+
from .authentication.token import (
|
|
15
|
+
AbstractToken,
|
|
16
|
+
NullToken,
|
|
17
|
+
TokenInterface,
|
|
18
|
+
UsernamePasswordToken,
|
|
19
|
+
)
|
|
20
|
+
from .authentication.token.storage import TokenStorage, TokenStorageInterface
|
|
21
|
+
from .authorization import (
|
|
22
|
+
Access,
|
|
23
|
+
AccessDecision,
|
|
24
|
+
AccessDecisionManager,
|
|
25
|
+
AccessDecisionManagerInterface,
|
|
26
|
+
AccessDecisionStrategyInterface,
|
|
27
|
+
AffirmativeStrategy,
|
|
28
|
+
AuthenticatedVoter,
|
|
29
|
+
AuthorizationChecker,
|
|
30
|
+
AuthorizationCheckerInterface,
|
|
31
|
+
CacheableVoterInterface,
|
|
32
|
+
ClosureVoter,
|
|
33
|
+
ConsensusStrategy,
|
|
34
|
+
GuestAuthorizationCheckerInterface,
|
|
35
|
+
IsGrantedContext,
|
|
36
|
+
PriorityStrategy,
|
|
37
|
+
RoleHierarchyVoter,
|
|
38
|
+
RoleVoter,
|
|
39
|
+
TraceableVoter,
|
|
40
|
+
UnanimousStrategy,
|
|
41
|
+
Vote,
|
|
42
|
+
Voter,
|
|
43
|
+
VoterInterface,
|
|
44
|
+
)
|
|
45
|
+
from .event import AuthenticationEvent, AuthenticationSuccessEvent, VoteEvent
|
|
46
|
+
from .exception import (
|
|
47
|
+
AccessDeniedError,
|
|
48
|
+
AccountExpiredError,
|
|
49
|
+
AccountStatusError,
|
|
50
|
+
AuthenticationCredentialsNotFoundError,
|
|
51
|
+
AuthenticationError,
|
|
52
|
+
AuthenticationServiceError,
|
|
53
|
+
BadCredentialsError,
|
|
54
|
+
CredentialsExpiredError,
|
|
55
|
+
CustomUserMessageAccountStatusError,
|
|
56
|
+
CustomUserMessageAuthenticationError,
|
|
57
|
+
DisabledError,
|
|
58
|
+
InsufficientAuthenticationError,
|
|
59
|
+
InvalidArgumentError,
|
|
60
|
+
LockedError,
|
|
61
|
+
SecurityError,
|
|
62
|
+
UnsupportedUserError,
|
|
63
|
+
UserNotFoundError,
|
|
64
|
+
)
|
|
65
|
+
from .role import RoleHierarchy, RoleHierarchyInterface
|
|
66
|
+
from .user import (
|
|
67
|
+
AttributesBasedUserProviderInterface,
|
|
68
|
+
ChainUserChecker,
|
|
69
|
+
ChainUserProvider,
|
|
70
|
+
EquatableInterface,
|
|
71
|
+
InMemoryUser,
|
|
72
|
+
InMemoryUserChecker,
|
|
73
|
+
InMemoryUserProvider,
|
|
74
|
+
OidcUser,
|
|
75
|
+
PasswordUpgraderInterface,
|
|
76
|
+
UserCheckerInterface,
|
|
77
|
+
UserInterface,
|
|
78
|
+
UserProviderInterface,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"AbstractToken",
|
|
83
|
+
"Access",
|
|
84
|
+
"AccessDecision",
|
|
85
|
+
"AccessDecisionManager",
|
|
86
|
+
"AccessDecisionManagerInterface",
|
|
87
|
+
"AccessDecisionStrategyInterface",
|
|
88
|
+
"AccessDeniedError",
|
|
89
|
+
"AccountExpiredError",
|
|
90
|
+
"AccountStatusError",
|
|
91
|
+
"AffirmativeStrategy",
|
|
92
|
+
"AttributesBasedUserProviderInterface",
|
|
93
|
+
"AuthenticatedVoter",
|
|
94
|
+
"AuthenticationCredentialsNotFoundError",
|
|
95
|
+
"AuthenticationError",
|
|
96
|
+
"AuthenticationEvent",
|
|
97
|
+
"AuthenticationServiceError",
|
|
98
|
+
"AuthenticationSuccessEvent",
|
|
99
|
+
"AuthenticationTrustResolver",
|
|
100
|
+
"AuthenticationTrustResolverInterface",
|
|
101
|
+
"AuthorizationChecker",
|
|
102
|
+
"AuthorizationCheckerInterface",
|
|
103
|
+
"BadCredentialsError",
|
|
104
|
+
"CacheableVoterInterface",
|
|
105
|
+
"ChainUserChecker",
|
|
106
|
+
"ChainUserProvider",
|
|
107
|
+
"ClosureVoter",
|
|
108
|
+
"ConsensusStrategy",
|
|
109
|
+
"CredentialsExpiredError",
|
|
110
|
+
"CustomUserMessageAccountStatusError",
|
|
111
|
+
"CustomUserMessageAuthenticationError",
|
|
112
|
+
"DisabledError",
|
|
113
|
+
"EquatableInterface",
|
|
114
|
+
"GuestAuthorizationCheckerInterface",
|
|
115
|
+
"InMemoryUser",
|
|
116
|
+
"InMemoryUserChecker",
|
|
117
|
+
"InMemoryUserProvider",
|
|
118
|
+
"InsufficientAuthenticationError",
|
|
119
|
+
"InvalidArgumentError",
|
|
120
|
+
"IsGrantedContext",
|
|
121
|
+
"LockedError",
|
|
122
|
+
"NullToken",
|
|
123
|
+
"OidcUser",
|
|
124
|
+
"PasswordUpgraderInterface",
|
|
125
|
+
"PriorityStrategy",
|
|
126
|
+
"RoleHierarchy",
|
|
127
|
+
"RoleHierarchyInterface",
|
|
128
|
+
"RoleHierarchyVoter",
|
|
129
|
+
"RoleVoter",
|
|
130
|
+
"SecurityError",
|
|
131
|
+
"TokenInterface",
|
|
132
|
+
"TokenStorage",
|
|
133
|
+
"TokenStorageInterface",
|
|
134
|
+
"TraceableVoter",
|
|
135
|
+
"UnanimousStrategy",
|
|
136
|
+
"UnsupportedUserError",
|
|
137
|
+
"UserCheckerInterface",
|
|
138
|
+
"UserInterface",
|
|
139
|
+
"UserNotFoundError",
|
|
140
|
+
"UserProviderInterface",
|
|
141
|
+
"UsernamePasswordToken",
|
|
142
|
+
"Vote",
|
|
143
|
+
"VoteEvent",
|
|
144
|
+
"Voter",
|
|
145
|
+
"VoterInterface",
|
|
146
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Authentication: tokens, where they are kept, and how strongly they are trusted."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .authentication_trust_resolver import AuthenticationTrustResolver
|
|
6
|
+
from .authentication_trust_resolver_interface import AuthenticationTrustResolverInterface
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AuthenticationTrustResolver",
|
|
10
|
+
"AuthenticationTrustResolverInterface",
|
|
11
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Tells how strongly a token was authenticated."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
|
|
7
|
+
from typing_extensions import override
|
|
8
|
+
|
|
9
|
+
from .authentication_trust_resolver_interface import AuthenticationTrustResolverInterface
|
|
10
|
+
from .token.null_token import NullToken
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from xtr_security_core.authentication.token.token_interface import TokenInterface
|
|
14
|
+
|
|
15
|
+
__all__ = ["AuthenticationTrustResolver"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
class AuthenticationTrustResolver(AuthenticationTrustResolverInterface):
|
|
20
|
+
"""Reads authentication strength from a token.
|
|
21
|
+
|
|
22
|
+
A token carries an authenticated user when it is neither absent nor the
|
|
23
|
+
:class:`~xtr_security_core.authentication.token.null_token.NullToken` that
|
|
24
|
+
stands for nobody. Without the remembered-me machinery this library does
|
|
25
|
+
not port, full authentication and authentication are the same question:
|
|
26
|
+
a real token is both, an anonymous one is neither.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
def is_authenticated(self, token: TokenInterface | None) -> bool:
|
|
31
|
+
"""Tell whether ``token`` carries an authenticated user at all."""
|
|
32
|
+
return (
|
|
33
|
+
token is not None and not isinstance(token, NullToken) and token.get_user() is not None
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
def is_full_fledged(self, token: TokenInterface | None) -> bool:
|
|
38
|
+
"""Tell whether ``token`` was fully authenticated this unit of work."""
|
|
39
|
+
return self.is_authenticated(token)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""What tells how strongly a token was authenticated."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from xtr_security_core.authentication.token.token_interface import TokenInterface
|
|
9
|
+
|
|
10
|
+
__all__ = ["AuthenticationTrustResolverInterface"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class AuthenticationTrustResolverInterface(Protocol):
|
|
15
|
+
"""Reads the strength of an authentication from a token.
|
|
16
|
+
|
|
17
|
+
A voter granting ``IS_AUTHENTICATED`` asks only whether anyone is behind
|
|
18
|
+
the token; one granting ``IS_AUTHENTICATED_FULLY`` asks whether they
|
|
19
|
+
authenticated this unit of work rather than being carried over. This
|
|
20
|
+
contract answers both without any voter having to know how a token was
|
|
21
|
+
made.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def is_authenticated(self, token: TokenInterface | None) -> bool:
|
|
25
|
+
"""Tell whether ``token`` carries an authenticated user at all."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def is_full_fledged(self, token: TokenInterface | None) -> bool:
|
|
29
|
+
"""Tell whether ``token`` was fully authenticated this unit of work."""
|
|
30
|
+
...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Tokens: the outcome of authentication, and where it is kept."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .abstract_token import AbstractToken
|
|
6
|
+
from .null_token import NullToken
|
|
7
|
+
from .token_interface import TokenInterface
|
|
8
|
+
from .username_password_token import UsernamePasswordToken
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AbstractToken",
|
|
12
|
+
"NullToken",
|
|
13
|
+
"TokenInterface",
|
|
14
|
+
"UsernamePasswordToken",
|
|
15
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""The shared base of every token: a user, fixed roles, and attributes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
from xtr_security_core.exception import InvalidArgumentError
|
|
11
|
+
|
|
12
|
+
from .token_interface import TokenInterface
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Mapping, Sequence
|
|
16
|
+
|
|
17
|
+
from xtr_security_core.user.user_interface import UserInterface
|
|
18
|
+
|
|
19
|
+
__all__ = ["AbstractToken"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AbstractToken(TokenInterface):
|
|
23
|
+
"""A user, the roles fixed at creation, and a bag of attributes.
|
|
24
|
+
|
|
25
|
+
Implements :class:`~xtr_security_core.authentication.token.token_interface.TokenInterface`.
|
|
26
|
+
The roles are copied at construction and never re-read from the user, so a
|
|
27
|
+
decision made against this token reflects what authentication settled on,
|
|
28
|
+
not what the user object may report afterwards. Concrete tokens derive from
|
|
29
|
+
this and add whatever their own kind needs.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_user: UserInterface | None
|
|
33
|
+
_role_names: tuple[str, ...]
|
|
34
|
+
_attributes: dict[str, object]
|
|
35
|
+
|
|
36
|
+
def __init__(self, user: UserInterface | None = None, roles: Sequence[str] = ()) -> None:
|
|
37
|
+
"""Record the user and copy the roles fixed on this token."""
|
|
38
|
+
self._user = user
|
|
39
|
+
self._role_names = tuple(roles)
|
|
40
|
+
self._attributes = {}
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
def get_user(self) -> UserInterface | None:
|
|
44
|
+
"""Return the authenticated user, or ``None`` for nobody."""
|
|
45
|
+
return self._user
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def get_user_identifier(self) -> str:
|
|
49
|
+
"""Return the identifier of the user, or the empty string for nobody."""
|
|
50
|
+
return self._user.get_user_identifier() if self._user is not None else ""
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
def get_role_names(self) -> Sequence[str]:
|
|
54
|
+
"""Return the roles fixed on this token at creation."""
|
|
55
|
+
return self._role_names
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
def get_attributes(self) -> Mapping[str, object]:
|
|
59
|
+
"""Return a read-only snapshot of the attributes attached to this token.
|
|
60
|
+
|
|
61
|
+
The mapping is a copy taken now, so a later
|
|
62
|
+
:meth:`set_attribute` does not change a mapping already returned.
|
|
63
|
+
"""
|
|
64
|
+
return MappingProxyType(dict(self._attributes))
|
|
65
|
+
|
|
66
|
+
@override
|
|
67
|
+
def get_attribute(self, name: str) -> object:
|
|
68
|
+
"""Return the attribute ``name``.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
InvalidArgumentError: When no attribute is attached under ``name``.
|
|
72
|
+
"""
|
|
73
|
+
if name not in self._attributes:
|
|
74
|
+
raise InvalidArgumentError(f'This token has no "{name}" attribute.')
|
|
75
|
+
return self._attributes[name]
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
def has_attribute(self, name: str) -> bool:
|
|
79
|
+
"""Tell whether an attribute is attached under ``name``."""
|
|
80
|
+
return name in self._attributes
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
def set_attribute(self, name: str, value: object) -> None:
|
|
84
|
+
"""Attach ``value`` under ``name``, replacing any attribute there."""
|
|
85
|
+
self._attributes[name] = value
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The token that stands for nobody."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import final
|
|
6
|
+
|
|
7
|
+
from .abstract_token import AbstractToken
|
|
8
|
+
|
|
9
|
+
__all__ = ["NullToken"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@final
|
|
13
|
+
class NullToken(AbstractToken):
|
|
14
|
+
"""A token carrying no user and no roles: the anonymous caller.
|
|
15
|
+
|
|
16
|
+
An access decision is always made against a token; when authentication
|
|
17
|
+
settled on nobody, it is made against this one. It reports no user, no
|
|
18
|
+
identifier and no roles, so a voter reading roles simply finds none.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
"""Build the token that stands for nobody."""
|
|
23
|
+
super().__init__(user=None, roles=())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Where the authenticated token lives for the current unit of work."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .token_storage import TokenStorage
|
|
6
|
+
from .token_storage_interface import TokenStorageInterface
|
|
7
|
+
|
|
8
|
+
__all__ = ["TokenStorage", "TokenStorageInterface"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""The token storage: one token per unit of work, cleared between them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
|
|
7
|
+
from typing_extensions import override
|
|
8
|
+
from xtr_service_contracts import ResetInterface
|
|
9
|
+
|
|
10
|
+
from .token_storage_interface import TokenStorageInterface
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from xtr_security_core.authentication.token.token_interface import TokenInterface
|
|
14
|
+
|
|
15
|
+
__all__ = ["TokenStorage"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
class TokenStorage(TokenStorageInterface, ResetInterface):
|
|
20
|
+
"""Holds the current token, and returns to empty between units of work.
|
|
21
|
+
|
|
22
|
+
A scoped service: one instance per request, message or command, so two
|
|
23
|
+
units of work never see each other's token. It satisfies
|
|
24
|
+
:class:`~xtr_service_contracts.reset_interface.ResetInterface` — a
|
|
25
|
+
container clears it when a unit of work ends, so the next starts with no
|
|
26
|
+
token — and is weakly referenceable because it declares no ``__slots__``,
|
|
27
|
+
so instances carry the ``__weakref__`` a kernel that resets its scoped
|
|
28
|
+
services requires.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_token: TokenInterface | None
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
"""Start with no token."""
|
|
35
|
+
self._token = None
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
def get_token(self) -> TokenInterface | None:
|
|
39
|
+
"""Return the current token, or ``None`` when none was set."""
|
|
40
|
+
return self._token
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
def set_token(self, token: TokenInterface | None) -> None:
|
|
44
|
+
"""Store ``token`` as the current one, or clear it with ``None``."""
|
|
45
|
+
self._token = token
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def reset(self) -> None:
|
|
49
|
+
"""Forget the current token, so the next unit of work starts clean."""
|
|
50
|
+
self._token = None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""What holds the token for the current unit of work."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from xtr_security_core.authentication.token.token_interface import TokenInterface
|
|
9
|
+
|
|
10
|
+
__all__ = ["TokenStorageInterface"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class TokenStorageInterface(Protocol):
|
|
15
|
+
"""The one place the authenticated token lives for a unit of work.
|
|
16
|
+
|
|
17
|
+
Authentication writes a token here; everything downstream — an access
|
|
18
|
+
decision, a facade reading the current user, a listener turning an error
|
|
19
|
+
into a response — reads it from here. Reading and writing are synchronous:
|
|
20
|
+
the token is already in hand by the time it is stored.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def get_token(self) -> TokenInterface | None:
|
|
24
|
+
"""Return the current token, or ``None`` when none was set."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def set_token(self, token: TokenInterface | None) -> None:
|
|
28
|
+
"""Store ``token`` as the current one, or clear it with ``None``."""
|
|
29
|
+
...
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""What a token — the outcome of authentication — answers to."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
from xtr_security_core.user.user_interface import UserInterface
|
|
11
|
+
|
|
12
|
+
__all__ = ["TokenInterface"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class TokenInterface(Protocol):
|
|
17
|
+
"""The result of authentication: who is calling, and what was decided about them.
|
|
18
|
+
|
|
19
|
+
A token names a user and the roles that authentication settled on. Those
|
|
20
|
+
roles are fixed when the token is created, decoupled from whatever the
|
|
21
|
+
live user object may report later, so a decision reads a stable snapshot.
|
|
22
|
+
Attributes carry extra data an authenticator or a listener attached — the
|
|
23
|
+
scopes of a bearer token, the client it was issued to.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def get_user(self) -> UserInterface | None:
|
|
27
|
+
"""Return the authenticated user, or ``None`` for nobody."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def get_user_identifier(self) -> str:
|
|
31
|
+
"""Return the identifier of the user, or the empty string for nobody."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def get_role_names(self) -> Sequence[str]:
|
|
35
|
+
"""Return the roles fixed on this token at creation."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def get_attributes(self) -> Mapping[str, object]:
|
|
39
|
+
"""Return every attribute attached to this token, read-only."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def get_attribute(self, name: str) -> object:
|
|
43
|
+
"""Return the attribute ``name``.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
InvalidArgumentError: When no attribute is attached under ``name``.
|
|
47
|
+
"""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def has_attribute(self, name: str) -> bool:
|
|
51
|
+
"""Tell whether an attribute is attached under ``name``."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def set_attribute(self, name: str, value: object) -> None:
|
|
55
|
+
"""Attach ``value`` under ``name``, replacing any attribute there."""
|
|
56
|
+
...
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""A token naming a user, the firewall that authenticated them, and their roles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
|
|
7
|
+
from .abstract_token import AbstractToken
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
|
|
12
|
+
from xtr_security_core.user.user_interface import UserInterface
|
|
13
|
+
|
|
14
|
+
__all__ = ["UsernamePasswordToken"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@final
|
|
18
|
+
class UsernamePasswordToken(AbstractToken):
|
|
19
|
+
"""A user proven by a password, the firewall that did it, and the roles.
|
|
20
|
+
|
|
21
|
+
The token an authentication settles on when a user answered with a
|
|
22
|
+
password. It carries the user, the firewall (or unit of work) that
|
|
23
|
+
authenticated them, and the roles fixed at that moment — decoupled from the
|
|
24
|
+
live user's roles, as every token in this library is.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
firewall_name: The firewall (or unit of work) that authenticated.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
user: UserInterface,
|
|
33
|
+
firewall_name: str,
|
|
34
|
+
roles: Sequence[str] = (),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Record the user, the firewall that authenticated them, and their roles."""
|
|
37
|
+
super().__init__(user=user, roles=roles)
|
|
38
|
+
self._firewall_name = firewall_name
|
|
39
|
+
|
|
40
|
+
def get_firewall_name(self) -> str:
|
|
41
|
+
"""Return the firewall (or unit of work) that authenticated the user."""
|
|
42
|
+
return self._firewall_name
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""The names the core security events are dispatched and listened to under.
|
|
2
|
+
|
|
3
|
+
Each event is keyed by its qualified class name (the repository's convention,
|
|
4
|
+
:func:`~xtr_event_dispatcher_contracts.event_name_of`). A subscriber listens to
|
|
5
|
+
one of these constants rather than importing the event class only to name it.
|
|
6
|
+
|
|
7
|
+
Only the core events live here — the success of an authentication, and a
|
|
8
|
+
voter's vote. The events raised at the HTTP edge are named by
|
|
9
|
+
:mod:`xtr_security_http.security_events`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Final
|
|
15
|
+
|
|
16
|
+
from xtr_event_dispatcher_contracts import event_name_of
|
|
17
|
+
|
|
18
|
+
from .event.authentication_success_event import AuthenticationSuccessEvent
|
|
19
|
+
from .event.vote_event import VoteEvent
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AUTHENTICATION_SUCCESS",
|
|
23
|
+
"VOTE",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
#: Dispatched once a token has been created for an authenticated user.
|
|
27
|
+
AUTHENTICATION_SUCCESS: Final[str] = event_name_of(AuthenticationSuccessEvent)
|
|
28
|
+
|
|
29
|
+
#: Dispatched by a traceable voter each time the voter it wraps answers.
|
|
30
|
+
VOTE: Final[str] = event_name_of(VoteEvent)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Authorization: voters, strategies, the decision manager and the checkers over them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .access_decision import AccessDecision
|
|
6
|
+
from .access_decision_manager import AccessDecisionManager
|
|
7
|
+
from .access_decision_manager_interface import AccessDecisionManagerInterface
|
|
8
|
+
from .authorization_checker import AuthorizationChecker
|
|
9
|
+
from .authorization_checker_interface import AuthorizationCheckerInterface
|
|
10
|
+
from .guest_authorization_checker_interface import GuestAuthorizationCheckerInterface
|
|
11
|
+
from .is_granted_context import IsGrantedContext
|
|
12
|
+
from .strategy import (
|
|
13
|
+
AccessDecisionStrategyInterface,
|
|
14
|
+
AffirmativeStrategy,
|
|
15
|
+
ConsensusStrategy,
|
|
16
|
+
PriorityStrategy,
|
|
17
|
+
UnanimousStrategy,
|
|
18
|
+
)
|
|
19
|
+
from .voter import (
|
|
20
|
+
Access,
|
|
21
|
+
AuthenticatedVoter,
|
|
22
|
+
CacheableVoterInterface,
|
|
23
|
+
ClosureVoter,
|
|
24
|
+
RoleHierarchyVoter,
|
|
25
|
+
RoleVoter,
|
|
26
|
+
TraceableVoter,
|
|
27
|
+
Vote,
|
|
28
|
+
Voter,
|
|
29
|
+
VoterInterface,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"Access",
|
|
34
|
+
"AccessDecision",
|
|
35
|
+
"AccessDecisionManager",
|
|
36
|
+
"AccessDecisionManagerInterface",
|
|
37
|
+
"AccessDecisionStrategyInterface",
|
|
38
|
+
"AffirmativeStrategy",
|
|
39
|
+
"AuthenticatedVoter",
|
|
40
|
+
"AuthorizationChecker",
|
|
41
|
+
"AuthorizationCheckerInterface",
|
|
42
|
+
"CacheableVoterInterface",
|
|
43
|
+
"ClosureVoter",
|
|
44
|
+
"ConsensusStrategy",
|
|
45
|
+
"GuestAuthorizationCheckerInterface",
|
|
46
|
+
"IsGrantedContext",
|
|
47
|
+
"PriorityStrategy",
|
|
48
|
+
"RoleHierarchyVoter",
|
|
49
|
+
"RoleVoter",
|
|
50
|
+
"TraceableVoter",
|
|
51
|
+
"UnanimousStrategy",
|
|
52
|
+
"Vote",
|
|
53
|
+
"Voter",
|
|
54
|
+
"VoterInterface",
|
|
55
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The record of one access decision: its votes and its outcome."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .voter.access import Access
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .voter.vote import Vote
|
|
12
|
+
|
|
13
|
+
__all__ = ["AccessDecision"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class AccessDecision:
|
|
18
|
+
"""What was decided, by which strategy, and every vote behind it.
|
|
19
|
+
|
|
20
|
+
A decision is not only a yes or no: it is the votes that produced it. The
|
|
21
|
+
manager fills one of these as the voters answer, so a refusal can be
|
|
22
|
+
explained — the :attr:`message` reads the denying voters' reasons. A nested
|
|
23
|
+
decision made while one is in progress shares the same record, so its votes
|
|
24
|
+
are gathered here too.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
is_granted: The outcome, once the manager has decided.
|
|
28
|
+
votes: Every vote cast, in the order they were cast.
|
|
29
|
+
strategy: The name of the strategy that decided, once set.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
is_granted: bool = False
|
|
33
|
+
votes: list[Vote] = field(default_factory=list)
|
|
34
|
+
strategy: str | None = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def message(self) -> str:
|
|
38
|
+
"""A short account of the decision: granted, or why it was denied."""
|
|
39
|
+
if self.is_granted:
|
|
40
|
+
return "Access granted."
|
|
41
|
+
reasons = [
|
|
42
|
+
reason for vote in self.votes if vote.result is Access.DENIED for reason in vote.reasons
|
|
43
|
+
]
|
|
44
|
+
if reasons:
|
|
45
|
+
return "Access denied. " + " ".join(reasons)
|
|
46
|
+
return "Access denied."
|