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.
Files changed (81) hide show
  1. xtr_security_core/__init__.py +146 -0
  2. xtr_security_core/authentication/__init__.py +11 -0
  3. xtr_security_core/authentication/authentication_trust_resolver.py +39 -0
  4. xtr_security_core/authentication/authentication_trust_resolver_interface.py +30 -0
  5. xtr_security_core/authentication/token/__init__.py +15 -0
  6. xtr_security_core/authentication/token/abstract_token.py +85 -0
  7. xtr_security_core/authentication/token/null_token.py +23 -0
  8. xtr_security_core/authentication/token/storage/__init__.py +8 -0
  9. xtr_security_core/authentication/token/storage/token_storage.py +50 -0
  10. xtr_security_core/authentication/token/storage/token_storage_interface.py +29 -0
  11. xtr_security_core/authentication/token/token_interface.py +56 -0
  12. xtr_security_core/authentication/token/username_password_token.py +42 -0
  13. xtr_security_core/authentication_events.py +30 -0
  14. xtr_security_core/authorization/__init__.py +55 -0
  15. xtr_security_core/authorization/access_decision.py +46 -0
  16. xtr_security_core/authorization/access_decision_manager.py +180 -0
  17. xtr_security_core/authorization/access_decision_manager_interface.py +40 -0
  18. xtr_security_core/authorization/authorization_checker.py +86 -0
  19. xtr_security_core/authorization/authorization_checker_interface.py +29 -0
  20. xtr_security_core/authorization/guest_authorization_checker_interface.py +33 -0
  21. xtr_security_core/authorization/is_granted_context.py +43 -0
  22. xtr_security_core/authorization/strategy/__init__.py +17 -0
  23. xtr_security_core/authorization/strategy/access_decision_strategy_interface.py +35 -0
  24. xtr_security_core/authorization/strategy/affirmative_strategy.py +54 -0
  25. xtr_security_core/authorization/strategy/consensus_strategy.py +61 -0
  26. xtr_security_core/authorization/strategy/priority_strategy.py +51 -0
  27. xtr_security_core/authorization/strategy/unanimous_strategy.py +54 -0
  28. xtr_security_core/authorization/voter/__init__.py +27 -0
  29. xtr_security_core/authorization/voter/access.py +21 -0
  30. xtr_security_core/authorization/voter/authenticated_voter.py +89 -0
  31. xtr_security_core/authorization/voter/cacheable_voter_interface.py +31 -0
  32. xtr_security_core/authorization/voter/closure_voter.py +91 -0
  33. xtr_security_core/authorization/voter/role_hierarchy_voter.py +39 -0
  34. xtr_security_core/authorization/voter/role_voter.py +70 -0
  35. xtr_security_core/authorization/voter/traceable_voter.py +87 -0
  36. xtr_security_core/authorization/voter/vote.py +35 -0
  37. xtr_security_core/authorization/voter/voter.py +80 -0
  38. xtr_security_core/authorization/voter/voter_interface.py +43 -0
  39. xtr_security_core/event/__init__.py +9 -0
  40. xtr_security_core/event/authentication_event.py +32 -0
  41. xtr_security_core/event/authentication_success_event.py +22 -0
  42. xtr_security_core/event/vote_event.py +41 -0
  43. xtr_security_core/exception/__init__.py +48 -0
  44. xtr_security_core/exception/access_denied_error.py +47 -0
  45. xtr_security_core/exception/account_expired_error.py +15 -0
  46. xtr_security_core/exception/account_status_error.py +41 -0
  47. xtr_security_core/exception/authentication_credentials_not_found_error.py +19 -0
  48. xtr_security_core/exception/authentication_error.py +61 -0
  49. xtr_security_core/exception/authentication_service_error.py +22 -0
  50. xtr_security_core/exception/bad_credentials_error.py +20 -0
  51. xtr_security_core/exception/credentials_expired_error.py +15 -0
  52. xtr_security_core/exception/custom_user_message_account_status_error.py +39 -0
  53. xtr_security_core/exception/custom_user_message_authentication_error.py +36 -0
  54. xtr_security_core/exception/disabled_error.py +15 -0
  55. xtr_security_core/exception/insufficient_authentication_error.py +19 -0
  56. xtr_security_core/exception/invalid_argument_error.py +18 -0
  57. xtr_security_core/exception/locked_error.py +15 -0
  58. xtr_security_core/exception/security_error.py +14 -0
  59. xtr_security_core/exception/unsupported_user_error.py +15 -0
  60. xtr_security_core/exception/user_not_found_error.py +44 -0
  61. xtr_security_core/py.typed +0 -0
  62. xtr_security_core/role/__init__.py +8 -0
  63. xtr_security_core/role/role_hierarchy.py +123 -0
  64. xtr_security_core/role/role_hierarchy_interface.py +28 -0
  65. xtr_security_core/user/__init__.py +43 -0
  66. xtr_security_core/user/attributes_based_user_provider_interface.py +41 -0
  67. xtr_security_core/user/chain_user_checker.py +57 -0
  68. xtr_security_core/user/chain_user_provider.py +110 -0
  69. xtr_security_core/user/equatable_interface.py +25 -0
  70. xtr_security_core/user/in_memory_user.py +87 -0
  71. xtr_security_core/user/in_memory_user_checker.py +48 -0
  72. xtr_security_core/user/in_memory_user_provider.py +96 -0
  73. xtr_security_core/user/oidc_user.py +66 -0
  74. xtr_security_core/user/password_upgrader_interface.py +33 -0
  75. xtr_security_core/user/user_checker_interface.py +43 -0
  76. xtr_security_core/user/user_interface.py +38 -0
  77. xtr_security_core/user/user_provider_interface.py +33 -0
  78. xtr_security_core-3.0.0.dist-info/METADATA +339 -0
  79. xtr_security_core-3.0.0.dist-info/RECORD +81 -0
  80. xtr_security_core-3.0.0.dist-info/WHEEL +4 -0
  81. 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."