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 +202 -0
- cadurso-0.0.1/README.md +185 -0
- cadurso-0.0.1/cadurso/__init__.py +3 -0
- cadurso-0.0.1/cadurso/errors.py +51 -0
- cadurso-0.0.1/cadurso/exceptions.py +26 -0
- cadurso-0.0.1/cadurso/py.typed +0 -0
- cadurso-0.0.1/cadurso/sugar.py +46 -0
- cadurso-0.0.1/cadurso/system.py +222 -0
- cadurso-0.0.1/cadurso/type_aliases.py +48 -0
- cadurso-0.0.1/pyproject.toml +29 -0
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
|
+
[](https://pypi.org/project/cadurso/)
|
|
21
|
+
[](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
|
+
|
cadurso-0.0.1/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# 🐻Cadurso[^1]
|
|
2
|
+
Authorization framework for Python-based applications. Inspired by _[Oso](https://github.com/osohq/oso)_.
|
|
3
|
+
|
|
4
|
+
[](https://pypi.org/project/cadurso/)
|
|
5
|
+
[](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,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"
|