cadurso 0.0.1__tar.gz

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.
cadurso-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.1
2
+ Name: cadurso
3
+ Version: 0.0.1
4
+ Summary: Authorization framework for Python-based applications
5
+ License: MIT
6
+ Author: cadu
7
+ Author-email: cadu.coelho@gmail.com
8
+ Requires-Python: >=3.11
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Description-Content-Type: text/markdown
16
+
17
+ # 🐻Cadurso[^1]
18
+ Authorization framework for Python-based applications. Inspired by _[Oso](https://github.com/osohq/oso)_.
19
+
20
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cadurso)](https://pypi.org/project/cadurso/)
21
+ [![PyPI - Version](https://img.shields.io/pypi/v/cadurso)](https://pypi.org/project/cadurso/)
22
+
23
+ ## Overview
24
+
25
+ Cadurso is a lightweight and extensible authorization framework designed to handle access control scenarios by building and querying rules. It enables developers to define rules for actors performing actions on resources, with support for synchronous and asynchronous workflows. This library is inspired by the principles of the Oso framework, emphasizing flexibility and clarity in managing authorization.
26
+
27
+ ## Features
28
+
29
+ - **Declarative Rule Definitions**: Define who can do what with ease. Rules are just Python functions[^2].
30
+ - **Support for Sync and Async**: Handle both blocking and non-blocking authorization queries seamlessly. Rules can also be async.
31
+ - **Immutable**: Prevent rule additions at runtime by freezing the authorization framework after defining rules.
32
+ - **Error Handling**: Comprehensive exceptions for incomplete queries, operational issues, and rule definition errors.
33
+
34
+ ## Use Cases
35
+
36
+ - Multi-tenant applications requiring fine-grained access control.
37
+ - Implementing Role-Based Access Control, Attribute-Based Access Control or anything in between.
38
+
39
+ ## Core Concepts
40
+
41
+ The core concepts of a Cadurso-powered authorization system are `Actors`, `Actions`, and `Resources`. They are combined into `Rules` which can be added to a `Cadurso` instance, represent capabilities within a system.
42
+
43
+ After defining rules, the framework can be marked as "frozen" to prevent further modifications, ensuring the integrity of the authorization system.
44
+
45
+ ### Actors
46
+
47
+ an `Actor` can be any Python instance. e.g `User`, `ServiceAccount`.
48
+
49
+ ### Actions
50
+
51
+ `Actions` are operations that `Actors` can attempt on `Resources`. They can be any hashable object.
52
+
53
+ Good candidates for `Actions` are `str`, `Enum`, etc.
54
+
55
+ _(But any object that implements `__hash__` and `__eq__` can be used)_
56
+
57
+ ### Resources
58
+
59
+ `Resources` are entities that `Actors` interact with. They can be any object that needs to be protected. e.g `Document`, `Post`.
60
+
61
+ ### Rules
62
+
63
+ `Rules` are combinations of `Actors`, `Actions`, and `Resources`. They are expressed as Python functions that return a boolean value.
64
+
65
+ Cadurso uses the type hints of the rule function to determine the types of the `Actor` and `Resource` arguments. The `Action` is passed as a parameter to the decorator that defines the rule.
66
+
67
+ Rule format:
68
+ ```python
69
+ cadurso = Cadurso()
70
+
71
+ @cadurso.add_rule(<ACTION>)
72
+ def rule_definition(actor: [ACTOR TYPE], resource: [RESOURCE TYPE]) -> bool:
73
+ # Return True or False based on the rule logic
74
+ ...
75
+ ```
76
+
77
+ ### State Freezing
78
+
79
+ Once you are finished defining rules, the framework should be "frozen" to prevent further modifications. This ensures the integrity of the authorization system.
80
+
81
+ ```python
82
+ cadurso.freeze()
83
+ ```
84
+
85
+ ## Quick Start
86
+
87
+ ### Installation
88
+
89
+ ```bash
90
+ pip install cadurso
91
+ ```
92
+
93
+ ### Complete Example
94
+
95
+ #### Defining Rules
96
+ ```python
97
+ # Initialize the authorization framework
98
+ from cadurso import Cadurso
99
+ cadurso = Cadurso()
100
+
101
+ # Some Actors and Resources type definitions
102
+ class User:
103
+ ...
104
+
105
+ class Document:
106
+ ...
107
+
108
+ # Some Actions
109
+ class DocumentPermission(Enum):
110
+ EDIT = auto()
111
+ """Edit a document."""
112
+
113
+ VIEW = auto()
114
+ """Visualize a document."""
115
+
116
+
117
+ # Define your authorization rules
118
+ @cadurso.add_rule(DocumentPermission.EDIT)
119
+ def owner_can_edit_own_document(actor: User, resource: Document) -> bool:
120
+ return actor == resource.owner
121
+
122
+ @cadurso.add_rule(DocumentPermission.EDIT)
123
+ def admin_can_edit_any_document(actor: User, _resource: Document) -> bool:
124
+ return actor.role == Role.ADMIN
125
+
126
+ @cadurso.add_rule(DocumentPermission.VIEW)
127
+ def owner_can_edit_own_document(actor: User, resource: Document) -> bool:
128
+ """Any person who can EDIT a document can also, obviously, VIEW it."""
129
+ # Piggyback on the EDIT permission.
130
+ # This way we don't need to write VIEW rules for both owners and admins.
131
+ return cadurso.can(actor).do(DocumentPermission.EDIT).on(resource)
132
+
133
+ # Async rules are also okay, if you need them
134
+ @cadurso.add_rule(DocumentPermission.VIEW)
135
+ async def async_rule(actor: User, resource: Document) -> bool:
136
+ return await some_other_async_check(actor, resource)
137
+
138
+
139
+ # Freeze the rules to prevent further modifications
140
+ cadurso.freeze()
141
+
142
+ # (You are ready to query now)
143
+ # Use your `cadurso` instance as a singleton throughout your application
144
+ ```
145
+
146
+ #### Querying
147
+
148
+ (Instance definitions)
149
+ ```python
150
+ # Some Actors
151
+ john = User(name="John", role=Role.USER)
152
+ gunnar = User(name="Gunnar", role=Role.ADMIN)
153
+
154
+ # Some Resources
155
+ johns_document = Document(owner=john)
156
+ gunnars_document = Document(owner=gunnar)
157
+ ```
158
+
159
+ (Query: Synchronous APIs)
160
+ ```python
161
+ # `.is_allowed()` method to query permissions
162
+ cadurso.is_allowed(john, DocumentPermission.EDIT, johns_document) # Output: True
163
+ cadurso.is_allowed(john, DocumentPermission.EDIT, gunnars_document) # Output: False
164
+ cadurso.is_allowed(gunnar, DocumentPermission.EDIT, johns_document) # Output: True
165
+
166
+ # Alternate querying syntax with `.can()`.
167
+ # This is just syntactic sugar for the above.
168
+ cadurso.can(john).do(DocumentPermission.EDIT).on(johns_document) # Output: True
169
+ cadurso.can(john).do(DocumentPermission.EDIT).on(gunnars_document) # Output: False
170
+ cadurso.can(gunnar).do(DocumentPermission.EDIT).on(johns_document) # Output: True
171
+ ```
172
+
173
+ (Query: Asynchronous APIs)
174
+ ```python
175
+ # `.is_allowed_async()` method to query permissions asynchronously
176
+ await cadurso.is_allowed_async(john, DocumentPermission.EDIT, johns_document) # Output: True
177
+
178
+ # Querying permissions with `.can()` asynchronously
179
+ await cadurso.can(john).do(DocumentPermission.EDIT).on_async(johns_document) # Output: True
180
+ ```
181
+
182
+ #### More examples?
183
+
184
+ - **ABAC** (Attribute-based Access Control) in Cadurso:
185
+ - Check [`/tests/akira/`](./tests/akira) for a full ABAC implementation set in the [Akira](https://en.wikipedia.org/wiki/Akira_(1988_film)) universe.
186
+
187
+
188
+ - **RBAC** (Role-based Access Control) in Cadurso:
189
+ - The [`/tests/brazil/`](./tests/brazil) folder shows a full RBAC implementation set in the [Brazil](https://en.wikipedia.org/wiki/Brazil_(1985_film)) universe.
190
+
191
+
192
+ ## Contributing
193
+ Contributions are welcome! Please ensure tests are included for any new features or bug fixes. Follow the standard pull request guidelines for this repository.
194
+
195
+ ## License
196
+ Cadurso is licensed under the MIT License. See the LICENSE file for details.
197
+
198
+ [^1]: Oso means "bear" in Spanish. `Cadurso` is a portmanteau of "Cadu" (my nickname) and "Urso" ("bear", in Portuguese) 😉
199
+
200
+ [^2]: **Important:** Rules should be pure functions, and avoid mutating the actors or resources passed to them.
201
+ As we cannot enforce this at runtime, it is the responsibility of the developer to ensure this.
202
+
@@ -0,0 +1,185 @@
1
+ # 🐻Cadurso[^1]
2
+ Authorization framework for Python-based applications. Inspired by _[Oso](https://github.com/osohq/oso)_.
3
+
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cadurso)](https://pypi.org/project/cadurso/)
5
+ [![PyPI - Version](https://img.shields.io/pypi/v/cadurso)](https://pypi.org/project/cadurso/)
6
+
7
+ ## Overview
8
+
9
+ Cadurso is a lightweight and extensible authorization framework designed to handle access control scenarios by building and querying rules. It enables developers to define rules for actors performing actions on resources, with support for synchronous and asynchronous workflows. This library is inspired by the principles of the Oso framework, emphasizing flexibility and clarity in managing authorization.
10
+
11
+ ## Features
12
+
13
+ - **Declarative Rule Definitions**: Define who can do what with ease. Rules are just Python functions[^2].
14
+ - **Support for Sync and Async**: Handle both blocking and non-blocking authorization queries seamlessly. Rules can also be async.
15
+ - **Immutable**: Prevent rule additions at runtime by freezing the authorization framework after defining rules.
16
+ - **Error Handling**: Comprehensive exceptions for incomplete queries, operational issues, and rule definition errors.
17
+
18
+ ## Use Cases
19
+
20
+ - Multi-tenant applications requiring fine-grained access control.
21
+ - Implementing Role-Based Access Control, Attribute-Based Access Control or anything in between.
22
+
23
+ ## Core Concepts
24
+
25
+ The core concepts of a Cadurso-powered authorization system are `Actors`, `Actions`, and `Resources`. They are combined into `Rules` which can be added to a `Cadurso` instance, represent capabilities within a system.
26
+
27
+ After defining rules, the framework can be marked as "frozen" to prevent further modifications, ensuring the integrity of the authorization system.
28
+
29
+ ### Actors
30
+
31
+ an `Actor` can be any Python instance. e.g `User`, `ServiceAccount`.
32
+
33
+ ### Actions
34
+
35
+ `Actions` are operations that `Actors` can attempt on `Resources`. They can be any hashable object.
36
+
37
+ Good candidates for `Actions` are `str`, `Enum`, etc.
38
+
39
+ _(But any object that implements `__hash__` and `__eq__` can be used)_
40
+
41
+ ### Resources
42
+
43
+ `Resources` are entities that `Actors` interact with. They can be any object that needs to be protected. e.g `Document`, `Post`.
44
+
45
+ ### Rules
46
+
47
+ `Rules` are combinations of `Actors`, `Actions`, and `Resources`. They are expressed as Python functions that return a boolean value.
48
+
49
+ Cadurso uses the type hints of the rule function to determine the types of the `Actor` and `Resource` arguments. The `Action` is passed as a parameter to the decorator that defines the rule.
50
+
51
+ Rule format:
52
+ ```python
53
+ cadurso = Cadurso()
54
+
55
+ @cadurso.add_rule(<ACTION>)
56
+ def rule_definition(actor: [ACTOR TYPE], resource: [RESOURCE TYPE]) -> bool:
57
+ # Return True or False based on the rule logic
58
+ ...
59
+ ```
60
+
61
+ ### State Freezing
62
+
63
+ Once you are finished defining rules, the framework should be "frozen" to prevent further modifications. This ensures the integrity of the authorization system.
64
+
65
+ ```python
66
+ cadurso.freeze()
67
+ ```
68
+
69
+ ## Quick Start
70
+
71
+ ### Installation
72
+
73
+ ```bash
74
+ pip install cadurso
75
+ ```
76
+
77
+ ### Complete Example
78
+
79
+ #### Defining Rules
80
+ ```python
81
+ # Initialize the authorization framework
82
+ from cadurso import Cadurso
83
+ cadurso = Cadurso()
84
+
85
+ # Some Actors and Resources type definitions
86
+ class User:
87
+ ...
88
+
89
+ class Document:
90
+ ...
91
+
92
+ # Some Actions
93
+ class DocumentPermission(Enum):
94
+ EDIT = auto()
95
+ """Edit a document."""
96
+
97
+ VIEW = auto()
98
+ """Visualize a document."""
99
+
100
+
101
+ # Define your authorization rules
102
+ @cadurso.add_rule(DocumentPermission.EDIT)
103
+ def owner_can_edit_own_document(actor: User, resource: Document) -> bool:
104
+ return actor == resource.owner
105
+
106
+ @cadurso.add_rule(DocumentPermission.EDIT)
107
+ def admin_can_edit_any_document(actor: User, _resource: Document) -> bool:
108
+ return actor.role == Role.ADMIN
109
+
110
+ @cadurso.add_rule(DocumentPermission.VIEW)
111
+ def owner_can_edit_own_document(actor: User, resource: Document) -> bool:
112
+ """Any person who can EDIT a document can also, obviously, VIEW it."""
113
+ # Piggyback on the EDIT permission.
114
+ # This way we don't need to write VIEW rules for both owners and admins.
115
+ return cadurso.can(actor).do(DocumentPermission.EDIT).on(resource)
116
+
117
+ # Async rules are also okay, if you need them
118
+ @cadurso.add_rule(DocumentPermission.VIEW)
119
+ async def async_rule(actor: User, resource: Document) -> bool:
120
+ return await some_other_async_check(actor, resource)
121
+
122
+
123
+ # Freeze the rules to prevent further modifications
124
+ cadurso.freeze()
125
+
126
+ # (You are ready to query now)
127
+ # Use your `cadurso` instance as a singleton throughout your application
128
+ ```
129
+
130
+ #### Querying
131
+
132
+ (Instance definitions)
133
+ ```python
134
+ # Some Actors
135
+ john = User(name="John", role=Role.USER)
136
+ gunnar = User(name="Gunnar", role=Role.ADMIN)
137
+
138
+ # Some Resources
139
+ johns_document = Document(owner=john)
140
+ gunnars_document = Document(owner=gunnar)
141
+ ```
142
+
143
+ (Query: Synchronous APIs)
144
+ ```python
145
+ # `.is_allowed()` method to query permissions
146
+ cadurso.is_allowed(john, DocumentPermission.EDIT, johns_document) # Output: True
147
+ cadurso.is_allowed(john, DocumentPermission.EDIT, gunnars_document) # Output: False
148
+ cadurso.is_allowed(gunnar, DocumentPermission.EDIT, johns_document) # Output: True
149
+
150
+ # Alternate querying syntax with `.can()`.
151
+ # This is just syntactic sugar for the above.
152
+ cadurso.can(john).do(DocumentPermission.EDIT).on(johns_document) # Output: True
153
+ cadurso.can(john).do(DocumentPermission.EDIT).on(gunnars_document) # Output: False
154
+ cadurso.can(gunnar).do(DocumentPermission.EDIT).on(johns_document) # Output: True
155
+ ```
156
+
157
+ (Query: Asynchronous APIs)
158
+ ```python
159
+ # `.is_allowed_async()` method to query permissions asynchronously
160
+ await cadurso.is_allowed_async(john, DocumentPermission.EDIT, johns_document) # Output: True
161
+
162
+ # Querying permissions with `.can()` asynchronously
163
+ await cadurso.can(john).do(DocumentPermission.EDIT).on_async(johns_document) # Output: True
164
+ ```
165
+
166
+ #### More examples?
167
+
168
+ - **ABAC** (Attribute-based Access Control) in Cadurso:
169
+ - Check [`/tests/akira/`](./tests/akira) for a full ABAC implementation set in the [Akira](https://en.wikipedia.org/wiki/Akira_(1988_film)) universe.
170
+
171
+
172
+ - **RBAC** (Role-based Access Control) in Cadurso:
173
+ - The [`/tests/brazil/`](./tests/brazil) folder shows a full RBAC implementation set in the [Brazil](https://en.wikipedia.org/wiki/Brazil_(1985_film)) universe.
174
+
175
+
176
+ ## Contributing
177
+ Contributions are welcome! Please ensure tests are included for any new features or bug fixes. Follow the standard pull request guidelines for this repository.
178
+
179
+ ## License
180
+ Cadurso is licensed under the MIT License. See the LICENSE file for details.
181
+
182
+ [^1]: Oso means "bear" in Spanish. `Cadurso` is a portmanteau of "Cadu" (my nickname) and "Urso" ("bear", in Portuguese) 😉
183
+
184
+ [^2]: **Important:** Rules should be pure functions, and avoid mutating the actors or resources passed to them.
185
+ As we cannot enforce this at runtime, it is the responsibility of the developer to ensure this.
@@ -0,0 +1,3 @@
1
+ from .system import Cadurso
2
+
3
+ __all__ = ["Cadurso"]
@@ -0,0 +1,51 @@
1
+ # Error messages
2
+ from typing import Any
3
+
4
+ ERROR_CANNOT_ADD_RULE_FROZEN = "Cannot add rules to a frozen Cadurso instance"
5
+
6
+ ERROR_AUTH_FUNC_NOT_CALLABLE = "Authorization function must be a callable object"
7
+
8
+ ERROR_AUTH_FUNC_WRONG_ARG_COUNT = (
9
+ "Authorization function must have two positional parameters"
10
+ )
11
+
12
+
13
+ def error_auth_func_wrong_number_of_parameters(param_count: int) -> str:
14
+ return f"{ERROR_AUTH_FUNC_WRONG_ARG_COUNT}, got {param_count}"
15
+
16
+
17
+ ERROR_AUTH_FUNC_ACTION_NOT_HASHABLE = (
18
+ "Authorization function's target action must be hashable"
19
+ )
20
+
21
+
22
+ ERROR_AUTH_FUNC_RETURN_NOT_BOOL_OR_BOOL_AWAITABLE = "Authorization function must return a boolean or an awaitable that returns a boolean"
23
+
24
+
25
+ def error_auth_func_return_not_bool_or_bool_awaitable(return_type: Any) -> str:
26
+ return f'{ERROR_AUTH_FUNC_RETURN_NOT_BOOL_OR_BOOL_AWAITABLE}, got "{return_type}"'
27
+
28
+
29
+ ERROR_AUTH_FUNC_PARAM_MUST_BE_POSITIONAL = (
30
+ "Authorization function parameters must be positional"
31
+ )
32
+
33
+
34
+ def error_auth_func_param_not_positional(param_kind: Any, param: Any) -> str:
35
+ return f"{ERROR_AUTH_FUNC_PARAM_MUST_BE_POSITIONAL}, got {param_kind} on {param}"
36
+
37
+
38
+ ERROR_AUTH_FUNC_TYPE_HINT_MUST_BE_TYPE = "type hint must be a type"
39
+
40
+
41
+ def error_auth_func_type_hint_not_type(identifier: str, hint_value: Any) -> str:
42
+ return f'{identifier} {ERROR_AUTH_FUNC_TYPE_HINT_MUST_BE_TYPE}, got "{hint_value}"'
43
+
44
+
45
+ ERROR_INSTANCE_NOT_FROZEN = (
46
+ "Cadurso instance must be frozen before querying permissions"
47
+ )
48
+
49
+ ERROR_CANNOT_FREEZE_ALREADY_FROZEN = "Cadurso instance is already frozen"
50
+
51
+ ERROR_QUERY_IS_INCOMPLETE = "Query is incomplete"
@@ -0,0 +1,26 @@
1
+ class CadursoBaseError(Exception):
2
+ pass
3
+
4
+
5
+ class CadursoRuleDefinitionError(CadursoBaseError):
6
+ """
7
+ Exception that gets raised when `Cadurso` is unable to ingest a new rule definition.
8
+ """
9
+
10
+ pass
11
+
12
+
13
+ class CadursoOperationalError(CadursoBaseError):
14
+ """
15
+ Exception that gets raised when `Cadurso` encounters an operational error.
16
+ """
17
+
18
+ pass
19
+
20
+
21
+ class CadursoIncompleteQueryError(CadursoBaseError):
22
+ """
23
+ Exception that gets raised when a query is incomplete and cannot be evaluated.
24
+ """
25
+
26
+ pass
File without changes
@@ -0,0 +1,46 @@
1
+ from dataclasses import dataclass
2
+ from typing import TYPE_CHECKING, Never
3
+
4
+ from cadurso.errors import ERROR_QUERY_IS_INCOMPLETE
5
+ from cadurso.exceptions import CadursoIncompleteQueryError
6
+ from cadurso.type_aliases import Action, Actor, AuthorizationDecision, Resource
7
+
8
+ if TYPE_CHECKING: # pragma: no cover
9
+ from cadurso import Cadurso
10
+
11
+
12
+ @dataclass
13
+ class OnResourceClause:
14
+ can_query: "CanQueryBuilder"
15
+ action: Action
16
+
17
+ def on(self, resource: Resource) -> AuthorizationDecision:
18
+ return self.can_query.cadurso.is_allowed(
19
+ self.can_query.actor, self.action, resource
20
+ )
21
+
22
+ async def on_async(self, resource: Resource) -> AuthorizationDecision:
23
+ return await self.can_query.cadurso.is_allowed_async(
24
+ self.can_query.actor, self.action, resource
25
+ )
26
+
27
+ def __bool__(self) -> Never:
28
+ """Avoid accidentally using the incomplete query as a boolean"""
29
+ raise CadursoIncompleteQueryError(
30
+ f"{ERROR_QUERY_IS_INCOMPLETE}: Do .on() or .on_async() to continue building it"
31
+ )
32
+
33
+
34
+ @dataclass
35
+ class CanQueryBuilder:
36
+ cadurso: "Cadurso"
37
+ actor: Actor
38
+
39
+ def do(self, action: Action) -> OnResourceClause:
40
+ return OnResourceClause(can_query=self, action=action)
41
+
42
+ def __bool__(self) -> Never:
43
+ """Avoid accidentally using the incomplete query as a boolean"""
44
+ raise CadursoIncompleteQueryError(
45
+ f"{ERROR_QUERY_IS_INCOMPLETE}: Do .do() to continue building it"
46
+ )
@@ -0,0 +1,222 @@
1
+ import asyncio
2
+ import inspect
3
+ import logging
4
+ from typing import Callable, Hashable, cast
5
+
6
+ from cadurso.errors import (
7
+ ERROR_AUTH_FUNC_ACTION_NOT_HASHABLE,
8
+ ERROR_AUTH_FUNC_NOT_CALLABLE,
9
+ ERROR_CANNOT_ADD_RULE_FROZEN,
10
+ ERROR_CANNOT_FREEZE_ALREADY_FROZEN,
11
+ ERROR_INSTANCE_NOT_FROZEN,
12
+ error_auth_func_param_not_positional,
13
+ error_auth_func_return_not_bool_or_bool_awaitable,
14
+ error_auth_func_type_hint_not_type,
15
+ error_auth_func_wrong_number_of_parameters,
16
+ )
17
+ from cadurso.exceptions import (
18
+ CadursoOperationalError,
19
+ CadursoRuleDefinitionError,
20
+ )
21
+ from cadurso.sugar import CanQueryBuilder
22
+ from cadurso.type_aliases import (
23
+ Action,
24
+ Actor,
25
+ ActorType,
26
+ AuthorizationDecision,
27
+ AuthorizationFunction,
28
+ Resource,
29
+ ResourceType,
30
+ RuleStorage,
31
+ RuleStorageKey,
32
+ )
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class Cadurso:
38
+ def __init__(self) -> None:
39
+ self.rule_count = 0
40
+ self.rule_storage: RuleStorage = {}
41
+
42
+ self.__frozen = False
43
+ """
44
+ A boolean indicating whether the Cadurso instance is frozen.
45
+
46
+ Frozen instances:
47
+ - Cannot have new rules added to them, and
48
+ - Cannot be unfrozen.
49
+ """
50
+
51
+ def __add(
52
+ self,
53
+ actor_type: ActorType,
54
+ action: Action,
55
+ resource_type: ResourceType,
56
+ authorization_function: AuthorizationFunction,
57
+ ) -> None:
58
+ storage_key: RuleStorageKey = (actor_type, action, resource_type)
59
+
60
+ self.rule_storage.setdefault(storage_key, set()).add(authorization_function)
61
+ self.rule_count += 1
62
+
63
+ def add_rule(
64
+ self, action: Action
65
+ ) -> Callable[[AuthorizationFunction], AuthorizationFunction]:
66
+ if self.__frozen:
67
+ raise CadursoRuleDefinitionError(ERROR_CANNOT_ADD_RULE_FROZEN)
68
+
69
+ def add_rule_inner(
70
+ authorization_function: AuthorizationFunction,
71
+ ) -> AuthorizationFunction:
72
+ # Check if action is hashable
73
+ if not isinstance(action, Hashable):
74
+ raise CadursoRuleDefinitionError(ERROR_AUTH_FUNC_ACTION_NOT_HASHABLE)
75
+
76
+ # Check if the authorization function is callable
77
+ if not callable(authorization_function):
78
+ raise CadursoRuleDefinitionError(ERROR_AUTH_FUNC_NOT_CALLABLE)
79
+
80
+ rule_func_signature = inspect.signature(authorization_function)
81
+
82
+ # Function should have, exactly, two parameters
83
+ if len(rule_func_signature.parameters) != 2:
84
+ raise CadursoRuleDefinitionError(
85
+ error_auth_func_wrong_number_of_parameters(
86
+ len(rule_func_signature.parameters)
87
+ )
88
+ )
89
+
90
+ # Extract actor and resource Parameter objects from the inspected function signature
91
+ # Actor is the first parameter, resource is the second
92
+ actor_parameter, resource_parameter = (
93
+ rule_func_signature.parameters.values()
94
+ )
95
+ identified_parameters = (
96
+ ("Actor", actor_parameter),
97
+ ("Resource", resource_parameter),
98
+ )
99
+
100
+ # The two inspected function arguments type hints must be:
101
+ # - Types,
102
+ # - which generate Hashable instances, and
103
+ # - presented as positional arguments
104
+ for identifier, parameter in identified_parameters:
105
+ if not isinstance(parameter.annotation, type):
106
+ raise CadursoRuleDefinitionError(
107
+ error_auth_func_type_hint_not_type(
108
+ identifier, parameter.annotation
109
+ )
110
+ )
111
+
112
+ if parameter.kind not in (
113
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
114
+ inspect.Parameter.POSITIONAL_ONLY,
115
+ ):
116
+ raise CadursoRuleDefinitionError(
117
+ error_auth_func_param_not_positional(parameter.kind, parameter)
118
+ )
119
+
120
+ # The return type must be boolean or an awaitable that returns a boolean
121
+ if not (
122
+ rule_func_signature.return_annotation is bool
123
+ or issubclass(rule_func_signature.return_annotation, asyncio.Future)
124
+ ):
125
+ raise CadursoRuleDefinitionError(
126
+ error_auth_func_return_not_bool_or_bool_awaitable(
127
+ rule_func_signature.return_annotation
128
+ )
129
+ )
130
+
131
+ # Add the rule to the rule storage
132
+ # Parameter.annotation is the actual content right of the colon in the function signature
133
+ self.__add(
134
+ actor_parameter.annotation,
135
+ action,
136
+ resource_parameter.annotation,
137
+ authorization_function,
138
+ )
139
+
140
+ return authorization_function
141
+
142
+ return add_rule_inner
143
+
144
+ def is_allowed(
145
+ self, actor: Actor, action: Action, resource: Resource
146
+ ) -> AuthorizationDecision:
147
+ """
148
+ Evaluate a permission query synchronously.
149
+ If there are async rules, this method use asyncio.run() to evaluate them.
150
+ If you are already in an async context, use is_allowed_async() instead.
151
+
152
+ :param actor: The actor that is trying to perform an action on a resource.
153
+ :param action: The action that the actor is trying to perform.
154
+ :param resource: The resource that the actor is trying to perform the action on.
155
+ :return: A boolean indicating whether the actor is allowed to perform the action on the resource.
156
+ """
157
+ if not self.__frozen:
158
+ raise CadursoOperationalError(ERROR_INSTANCE_NOT_FROZEN)
159
+
160
+ for rule in self.rule_storage.get((type(actor), action, type(resource)), []):
161
+ if inspect.iscoroutinefunction(rule):
162
+ decision = cast(bool, asyncio.run(rule(actor, resource)))
163
+ else:
164
+ decision = cast(bool, rule(actor, resource))
165
+
166
+ if decision is not True:
167
+ logger.debug(
168
+ f'"{actor}" is not allowed to "{action}" on "{resource}", trying other rules...'
169
+ )
170
+ continue
171
+
172
+ logger.debug(f'"{actor}" is allowed to "{action}" on "{resource}"')
173
+ return True
174
+
175
+ return False
176
+
177
+ async def is_allowed_async(
178
+ self, actor: Actor, action: Action, resource: Resource
179
+ ) -> AuthorizationDecision:
180
+ """
181
+ Evaluate a permission query asynchronously.
182
+
183
+ :param actor: The actor that is trying to perform an action on a resource.
184
+ :param action: The action that the actor is trying to perform.
185
+ :param resource: The resource that the actor is trying to perform the action on.
186
+ :return: A boolean indicating whether the actor is allowed to perform the action on the resource.
187
+ """
188
+ for rule in self.rule_storage.get((type(actor), action, type(resource)), []):
189
+ if inspect.iscoroutinefunction(rule):
190
+ decision = await rule(actor, resource)
191
+ else:
192
+ decision = rule(actor, resource)
193
+
194
+ if decision is not True:
195
+ logger.debug(
196
+ f'"{actor}" is not allowed to "{action}" on "{resource}", trying other rules...'
197
+ )
198
+ continue
199
+
200
+ logger.debug(f'"{actor}" is allowed to "{action}" on "{resource}"')
201
+ return True
202
+
203
+ return False
204
+
205
+ def can(self, actor: Actor) -> CanQueryBuilder:
206
+ """
207
+ Syntax sugar for asking permissions (Query Builder-style).
208
+ """
209
+ return CanQueryBuilder(cadurso=self, actor=actor)
210
+
211
+ def freeze(self) -> None:
212
+ """
213
+ Freeze the Cadurso instance, preventing new rules from being added.
214
+ """
215
+
216
+ if self.__frozen:
217
+ raise CadursoOperationalError(ERROR_CANNOT_FREEZE_ALREADY_FROZEN)
218
+
219
+ self.__frozen = True
220
+
221
+ # Log the number of rules added
222
+ logger.debug(f"Frozen Cadurso instance with {self.rule_count} rules")
@@ -0,0 +1,48 @@
1
+ from typing import Any, Awaitable, Callable, Hashable
2
+
3
+ Actor = Any
4
+ """
5
+ An Actor is an entity (instance) that will be granted or denied access to a resource.
6
+ """
7
+
8
+ ActorType = type[Actor]
9
+ """
10
+ An ActorType is a type that represents an Actor.
11
+ """
12
+
13
+ Action = Hashable
14
+ """
15
+ An Action that an Actor can perform on a Resource.
16
+
17
+ Anything that implements __hash__ can be used as an ActionType.
18
+ """
19
+
20
+ Resource = Any
21
+ """
22
+ A Resource that an Actor can perform an Action on.
23
+ """
24
+
25
+ ResourceType = type[Resource]
26
+ """
27
+ A ResourceType is a type that represents a Resource.
28
+ """
29
+
30
+ AuthorizationDecision = bool
31
+ """
32
+ An AuthorizationDecision is a boolean that indicates whether an Actor is allowed to perform an Action on a Resource.
33
+ """
34
+
35
+ AuthorizationFunction = (
36
+ Callable[[Actor, Resource], AuthorizationDecision]
37
+ | Callable[[Actor, Resource], Awaitable[AuthorizationDecision]]
38
+ )
39
+ """
40
+ An AuthorizationFunction is a function that takes an Actor and a Resource and returns (synchronously or asynchronously)
41
+ an AuthorizationDecision, representing the decision to allow or deny the Actor to perform an Action on the Resource.
42
+ """
43
+
44
+ RuleStorageKey = tuple[ActorType, Action, ResourceType]
45
+ RuleStorage = dict[RuleStorageKey, set[AuthorizationFunction]]
46
+ """
47
+ A RuleStorage is a dictionary that maps a tuple of an Actor, an Action, and a Resource to a set of AuthorizationFunctions.
48
+ """
@@ -0,0 +1,29 @@
1
+ [tool.poetry]
2
+ name = "cadurso"
3
+ version = "0.0.1"
4
+ description = "Authorization framework for Python-based applications"
5
+ authors = ["cadu <cadu.coelho@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ classifiers = [
10
+ "Development Status :: 5 - Production/Stable",
11
+ "Programming Language :: Python :: 3.11",
12
+ "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13"
14
+ ]
15
+
16
+ [tool.poetry.dependencies]
17
+ python = ">=3.11"
18
+
19
+
20
+ [tool.poetry.group.dev.dependencies]
21
+ pytest = "^8.3.4"
22
+ pre-commit = "^4.0.1"
23
+ pytest-asyncio = "^0.25.0"
24
+ mypy = "^1.14.0"
25
+ pytest-cov = "^6.0.0"
26
+
27
+ [build-system]
28
+ requires = ["poetry-core"]
29
+ build-backend = "poetry.core.masonry.api"