proxilion 0.0.1__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.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in policies for Proxilion.
|
|
3
|
+
|
|
4
|
+
This module provides commonly used policy implementations that can be
|
|
5
|
+
used directly or extended for custom authorization logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from proxilion.policies.base import Policy
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from proxilion.types import UserContext
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DenyAllPolicy(Policy[Any]):
|
|
22
|
+
"""
|
|
23
|
+
Policy that denies all actions.
|
|
24
|
+
|
|
25
|
+
This is the safest default policy - it denies everything unless
|
|
26
|
+
explicitly allowed. Use this as a base class or as the default
|
|
27
|
+
policy in the registry to ensure secure-by-default behavior.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> registry = PolicyRegistry(default_policy=DenyAllPolicy)
|
|
31
|
+
>>> # Any unregistered resource will be denied
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def can_execute(self, context: dict[str, Any]) -> bool:
|
|
35
|
+
"""Deny execution."""
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def can_read(self, context: dict[str, Any]) -> bool:
|
|
39
|
+
"""Deny read access."""
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def can_write(self, context: dict[str, Any]) -> bool:
|
|
43
|
+
"""Deny write access."""
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
def can_delete(self, context: dict[str, Any]) -> bool:
|
|
47
|
+
"""Deny delete access."""
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def can_create(self, context: dict[str, Any]) -> bool:
|
|
51
|
+
"""Deny create access."""
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def can_update(self, context: dict[str, Any]) -> bool:
|
|
55
|
+
"""Deny update access."""
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def can_list(self, context: dict[str, Any]) -> bool:
|
|
59
|
+
"""Deny list access."""
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def authorize(self, action: str, context: dict[str, Any] | None = None) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Override authorize to deny all actions.
|
|
65
|
+
|
|
66
|
+
Even actions without explicit can_<action> methods are denied.
|
|
67
|
+
"""
|
|
68
|
+
logger.debug(
|
|
69
|
+
f"DenyAllPolicy: Denying action '{action}' for user '{self.user.user_id}'"
|
|
70
|
+
)
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AllowAllPolicy(Policy[Any]):
|
|
75
|
+
"""
|
|
76
|
+
Policy that allows all actions.
|
|
77
|
+
|
|
78
|
+
WARNING: This policy should ONLY be used for testing or in
|
|
79
|
+
development environments. Using it in production is a security risk.
|
|
80
|
+
|
|
81
|
+
A warning is logged every time this policy is instantiated to
|
|
82
|
+
help catch accidental production usage.
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> # For testing only!
|
|
86
|
+
>>> @registry.policy("test_resource")
|
|
87
|
+
>>> class TestPolicy(AllowAllPolicy):
|
|
88
|
+
... pass
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, user: UserContext, resource: Any = None) -> None:
|
|
92
|
+
"""Initialize with a security warning."""
|
|
93
|
+
super().__init__(user, resource)
|
|
94
|
+
logger.warning(
|
|
95
|
+
f"AllowAllPolicy instantiated for user '{user.user_id}'. "
|
|
96
|
+
"This policy allows ALL actions and should NOT be used in production!"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def can_execute(self, context: dict[str, Any]) -> bool:
|
|
100
|
+
"""Allow execution."""
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
def can_read(self, context: dict[str, Any]) -> bool:
|
|
104
|
+
"""Allow read access."""
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def can_write(self, context: dict[str, Any]) -> bool:
|
|
108
|
+
"""Allow write access."""
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
def can_delete(self, context: dict[str, Any]) -> bool:
|
|
112
|
+
"""Allow delete access."""
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def can_create(self, context: dict[str, Any]) -> bool:
|
|
116
|
+
"""Allow create access."""
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
def can_update(self, context: dict[str, Any]) -> bool:
|
|
120
|
+
"""Allow update access."""
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def can_list(self, context: dict[str, Any]) -> bool:
|
|
124
|
+
"""Allow list access."""
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
def authorize(self, action: str, context: dict[str, Any] | None = None) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Override authorize to allow all actions.
|
|
130
|
+
|
|
131
|
+
Even actions without explicit can_<action> methods are allowed.
|
|
132
|
+
"""
|
|
133
|
+
logger.debug(
|
|
134
|
+
f"AllowAllPolicy: Allowing action '{action}' for user '{self.user.user_id}'"
|
|
135
|
+
)
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class RoleBasedPolicy(Policy[Any]):
|
|
140
|
+
"""
|
|
141
|
+
Base class for role-based authorization policies.
|
|
142
|
+
|
|
143
|
+
This policy checks if the user has any of the required roles
|
|
144
|
+
for a given action. Subclasses define the role requirements
|
|
145
|
+
by setting the `allowed_roles` class attribute.
|
|
146
|
+
|
|
147
|
+
Attributes:
|
|
148
|
+
allowed_roles: Dictionary mapping action names to lists of
|
|
149
|
+
roles that are allowed to perform that action.
|
|
150
|
+
default_allowed: Whether to allow actions not in allowed_roles.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> class DocumentPolicy(RoleBasedPolicy):
|
|
154
|
+
... allowed_roles = {
|
|
155
|
+
... "read": ["viewer", "editor", "admin"],
|
|
156
|
+
... "write": ["editor", "admin"],
|
|
157
|
+
... "delete": ["admin"],
|
|
158
|
+
... }
|
|
159
|
+
>>>
|
|
160
|
+
>>> # User with "editor" role can read and write, but not delete
|
|
161
|
+
>>> policy = DocumentPolicy(user_with_editor_role, document)
|
|
162
|
+
>>> policy.can("read") # True
|
|
163
|
+
>>> policy.can("write") # True
|
|
164
|
+
>>> policy.can("delete") # False
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
# Subclasses should override this
|
|
168
|
+
allowed_roles: dict[str, list[str]] = {}
|
|
169
|
+
|
|
170
|
+
# Whether to allow actions not explicitly listed in allowed_roles
|
|
171
|
+
default_allowed: bool = False
|
|
172
|
+
|
|
173
|
+
def authorize(self, action: str, context: dict[str, Any] | None = None) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Check if user has a required role for the action.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
action: The action to check.
|
|
179
|
+
context: Additional context (unused in basic role check).
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if user has a required role, False otherwise.
|
|
183
|
+
"""
|
|
184
|
+
if action not in self.allowed_roles:
|
|
185
|
+
if self.default_allowed:
|
|
186
|
+
logger.debug(
|
|
187
|
+
f"RoleBasedPolicy: Action '{action}' not in allowed_roles, "
|
|
188
|
+
f"allowing by default"
|
|
189
|
+
)
|
|
190
|
+
return True
|
|
191
|
+
logger.debug(
|
|
192
|
+
f"RoleBasedPolicy: Action '{action}' not in allowed_roles, "
|
|
193
|
+
f"denying by default"
|
|
194
|
+
)
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
required_roles = self.allowed_roles[action]
|
|
198
|
+
user_roles = set(self.user.roles)
|
|
199
|
+
allowed_role_set = set(required_roles)
|
|
200
|
+
|
|
201
|
+
has_role = bool(user_roles & allowed_role_set)
|
|
202
|
+
|
|
203
|
+
if has_role:
|
|
204
|
+
matching = user_roles & allowed_role_set
|
|
205
|
+
logger.debug(
|
|
206
|
+
f"RoleBasedPolicy: User '{self.user.user_id}' has role(s) "
|
|
207
|
+
f"{matching} for action '{action}'"
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
logger.debug(
|
|
211
|
+
f"RoleBasedPolicy: User '{self.user.user_id}' lacks required "
|
|
212
|
+
f"role(s) {required_roles} for action '{action}'"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return has_role
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def with_roles(
|
|
219
|
+
cls,
|
|
220
|
+
roles: dict[str, list[str]],
|
|
221
|
+
default_allowed: bool = False,
|
|
222
|
+
) -> type[RoleBasedPolicy]:
|
|
223
|
+
"""
|
|
224
|
+
Create a RoleBasedPolicy subclass with specific roles.
|
|
225
|
+
|
|
226
|
+
Factory method for creating role-based policies dynamically.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
roles: Dictionary mapping actions to allowed roles.
|
|
230
|
+
default_allowed: Whether to allow unlisted actions.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
A new RoleBasedPolicy subclass.
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
>>> ApiPolicy = RoleBasedPolicy.with_roles({
|
|
237
|
+
... "read": ["user", "admin"],
|
|
238
|
+
... "write": ["admin"],
|
|
239
|
+
... })
|
|
240
|
+
>>> policy = ApiPolicy(user, api_resource)
|
|
241
|
+
"""
|
|
242
|
+
class DynamicRolePolicy(cls): # type: ignore[valid-type, misc]
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
DynamicRolePolicy.allowed_roles = roles
|
|
246
|
+
DynamicRolePolicy.default_allowed = default_allowed
|
|
247
|
+
return DynamicRolePolicy
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class AttributeBasedPolicy(Policy[Any]):
|
|
251
|
+
"""
|
|
252
|
+
Base class for attribute-based authorization policies.
|
|
253
|
+
|
|
254
|
+
This policy allows defining rules based on user attributes,
|
|
255
|
+
resource attributes, and environmental conditions. More flexible
|
|
256
|
+
than role-based policies but more complex to configure.
|
|
257
|
+
|
|
258
|
+
Subclasses should override the `evaluate_rules` method or
|
|
259
|
+
set the `rules` class attribute.
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
>>> class DocumentPolicy(AttributeBasedPolicy):
|
|
263
|
+
... def evaluate_rules(
|
|
264
|
+
... self, action: str, context: dict
|
|
265
|
+
... ) -> bool:
|
|
266
|
+
... # Allow if user is owner
|
|
267
|
+
... if self.resource.owner_id == self.user.user_id:
|
|
268
|
+
... return True
|
|
269
|
+
... # Allow if user is in same department
|
|
270
|
+
... if self.user.get_attribute("department") == \
|
|
271
|
+
... self.resource.department:
|
|
272
|
+
... return action == "read"
|
|
273
|
+
... return False
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def evaluate_rules(
|
|
277
|
+
self,
|
|
278
|
+
action: str,
|
|
279
|
+
context: dict[str, Any],
|
|
280
|
+
) -> bool:
|
|
281
|
+
"""
|
|
282
|
+
Evaluate authorization rules.
|
|
283
|
+
|
|
284
|
+
Override this method to implement custom attribute-based logic.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
action: The action being checked.
|
|
288
|
+
context: Additional context for the decision.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
True if authorized, False otherwise.
|
|
292
|
+
"""
|
|
293
|
+
# Default implementation denies all
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
def authorize(self, action: str, context: dict[str, Any] | None = None) -> bool:
|
|
297
|
+
"""
|
|
298
|
+
Check authorization using attribute-based rules.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
action: The action to check.
|
|
302
|
+
context: Additional context for the decision.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
True if authorized, False otherwise.
|
|
306
|
+
"""
|
|
307
|
+
ctx = context or {}
|
|
308
|
+
|
|
309
|
+
# Add user attributes to context for convenience
|
|
310
|
+
ctx["user_id"] = self.user.user_id
|
|
311
|
+
ctx["user_roles"] = self.user.roles
|
|
312
|
+
ctx["user_attributes"] = self.user.attributes
|
|
313
|
+
|
|
314
|
+
result = self.evaluate_rules(action, ctx)
|
|
315
|
+
|
|
316
|
+
logger.debug(
|
|
317
|
+
f"AttributeBasedPolicy: Action '{action}' for user "
|
|
318
|
+
f"'{self.user.user_id}' -> {'allowed' if result else 'denied'}"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class OwnershipPolicy(Policy[Any]):
|
|
325
|
+
"""
|
|
326
|
+
Policy that allows actions only if the user owns the resource.
|
|
327
|
+
|
|
328
|
+
This policy checks if the resource has an owner_id or user_id
|
|
329
|
+
attribute that matches the user's ID.
|
|
330
|
+
|
|
331
|
+
Attributes:
|
|
332
|
+
owner_field: Name of the attribute on the resource that contains
|
|
333
|
+
the owner's user ID. Defaults to "owner_id".
|
|
334
|
+
owner_actions: Actions that require ownership. Other actions
|
|
335
|
+
may be allowed based on `allow_non_owner_actions`.
|
|
336
|
+
allow_non_owner_actions: List of actions allowed for non-owners.
|
|
337
|
+
|
|
338
|
+
Example:
|
|
339
|
+
>>> class DocumentPolicy(OwnershipPolicy):
|
|
340
|
+
... owner_field = "created_by"
|
|
341
|
+
... allow_non_owner_actions = ["read"] # Anyone can read
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
owner_field: str = "owner_id"
|
|
345
|
+
owner_actions: list[str] = ["write", "delete", "update"]
|
|
346
|
+
allow_non_owner_actions: list[str] = []
|
|
347
|
+
|
|
348
|
+
def is_owner(self) -> bool:
|
|
349
|
+
"""Check if the user owns the resource."""
|
|
350
|
+
if self.resource is None:
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
owner_id = getattr(self.resource, self.owner_field, None)
|
|
354
|
+
if owner_id is None:
|
|
355
|
+
# Try alternative field names
|
|
356
|
+
owner_id = getattr(self.resource, "user_id", None)
|
|
357
|
+
|
|
358
|
+
return owner_id == self.user.user_id
|
|
359
|
+
|
|
360
|
+
def authorize(self, action: str, context: dict[str, Any] | None = None) -> bool:
|
|
361
|
+
"""
|
|
362
|
+
Check if user is authorized based on ownership.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
action: The action to check.
|
|
366
|
+
context: Additional context (unused).
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
True if user is owner or action is in allow_non_owner_actions.
|
|
370
|
+
"""
|
|
371
|
+
if action in self.allow_non_owner_actions:
|
|
372
|
+
logger.debug(
|
|
373
|
+
f"OwnershipPolicy: Action '{action}' allowed for non-owners"
|
|
374
|
+
)
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
is_owner = self.is_owner()
|
|
378
|
+
|
|
379
|
+
if is_owner:
|
|
380
|
+
logger.debug(
|
|
381
|
+
f"OwnershipPolicy: User '{self.user.user_id}' is owner, "
|
|
382
|
+
f"allowing action '{action}'"
|
|
383
|
+
)
|
|
384
|
+
else:
|
|
385
|
+
logger.debug(
|
|
386
|
+
f"OwnershipPolicy: User '{self.user.user_id}' is not owner, "
|
|
387
|
+
f"denying action '{action}'"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return is_owner
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class CompositePolicy(Policy[Any]):
|
|
394
|
+
"""
|
|
395
|
+
Policy that combines multiple policies with AND/OR logic.
|
|
396
|
+
|
|
397
|
+
Use this to create complex authorization rules from simpler policies.
|
|
398
|
+
|
|
399
|
+
Attributes:
|
|
400
|
+
policies: List of policy classes to combine.
|
|
401
|
+
require_all: If True, all policies must allow (AND).
|
|
402
|
+
If False, any policy can allow (OR).
|
|
403
|
+
|
|
404
|
+
Example:
|
|
405
|
+
>>> class SecureDocumentPolicy(CompositePolicy):
|
|
406
|
+
... policies = [RoleBasedPolicy, OwnershipPolicy]
|
|
407
|
+
... require_all = True # Must have role AND be owner
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
policies: list[type[Policy[Any]]] = []
|
|
411
|
+
require_all: bool = True
|
|
412
|
+
|
|
413
|
+
def authorize(self, action: str, context: dict[str, Any] | None = None) -> bool:
|
|
414
|
+
"""
|
|
415
|
+
Check authorization using all configured policies.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
action: The action to check.
|
|
419
|
+
context: Additional context passed to each policy.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
True if authorized according to combination logic.
|
|
423
|
+
"""
|
|
424
|
+
if not self.policies:
|
|
425
|
+
logger.warning("CompositePolicy has no policies configured")
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
results = []
|
|
429
|
+
for policy_class in self.policies:
|
|
430
|
+
policy = policy_class(self.user, self.resource)
|
|
431
|
+
result = policy.authorize(action, context)
|
|
432
|
+
results.append(result)
|
|
433
|
+
|
|
434
|
+
# Short-circuit evaluation
|
|
435
|
+
if self.require_all and not result:
|
|
436
|
+
logger.debug(
|
|
437
|
+
f"CompositePolicy: {policy_class.__name__} denied "
|
|
438
|
+
f"action '{action}' (require_all=True)"
|
|
439
|
+
)
|
|
440
|
+
return False
|
|
441
|
+
if not self.require_all and result:
|
|
442
|
+
logger.debug(
|
|
443
|
+
f"CompositePolicy: {policy_class.__name__} allowed "
|
|
444
|
+
f"action '{action}' (require_all=False)"
|
|
445
|
+
)
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
# If we get here with require_all=True, all passed
|
|
449
|
+
# If we get here with require_all=False, all failed
|
|
450
|
+
final_result = self.require_all
|
|
451
|
+
logger.debug(
|
|
452
|
+
f"CompositePolicy: Final result for action '{action}': "
|
|
453
|
+
f"{'allowed' if final_result else 'denied'}"
|
|
454
|
+
)
|
|
455
|
+
return final_result
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def combine(
|
|
459
|
+
cls,
|
|
460
|
+
*policy_classes: type[Policy[Any]],
|
|
461
|
+
require_all: bool = True,
|
|
462
|
+
) -> type[CompositePolicy]:
|
|
463
|
+
"""
|
|
464
|
+
Create a CompositePolicy combining multiple policies.
|
|
465
|
+
|
|
466
|
+
Factory method for creating composite policies dynamically.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
*policy_classes: Policy classes to combine.
|
|
470
|
+
require_all: Whether all must allow (AND) or any (OR).
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
A new CompositePolicy subclass.
|
|
474
|
+
|
|
475
|
+
Example:
|
|
476
|
+
>>> CombinedPolicy = CompositePolicy.combine(
|
|
477
|
+
... RolePolicy, OwnerPolicy,
|
|
478
|
+
... require_all=True
|
|
479
|
+
... )
|
|
480
|
+
"""
|
|
481
|
+
class DynamicCompositePolicy(cls):
|
|
482
|
+
pass
|
|
483
|
+
|
|
484
|
+
DynamicCompositePolicy.policies = list(policy_classes)
|
|
485
|
+
DynamicCompositePolicy.require_all = require_all
|
|
486
|
+
return DynamicCompositePolicy
|