Skip to content

Commit a49e412

Browse files
author
Mathias BIGAIGNON
committed
Initial rule rework
1 parent 6ccede0 commit a49e412

File tree

5 files changed

+143
-25
lines changed

5 files changed

+143
-25
lines changed

entitled/client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,34 @@ def __init__(self, base_path: str | None = None):
1818
self._load_path = pathlib.Path(base_path)
1919
self.load_policies_from_path(self._load_path)
2020

21-
def authorize(
21+
async def authorize(
2222
self,
2323
action: str,
2424
actor: Any,
2525
resource: Any,
2626
context: dict[str, Any] | None = None,
2727
) -> bool:
2828
policy = self._policy_lookup(resource)
29-
return policy.authorize(action, actor, resource, context)
29+
return await policy.authorize(action, actor, resource, context)
3030

31-
def allows(
31+
async def allows(
3232
self,
3333
action: str,
3434
actor: Any,
3535
resource: Any,
3636
context: dict[str, Any] | None = None,
3737
) -> bool:
3838
policy = self._policy_lookup(resource)
39-
return policy.allows(action, actor, resource, context)
39+
return await policy.allows(action, actor, resource, context)
4040

41-
def grants(
41+
async def grants(
4242
self,
4343
actor: Any,
4444
resource: Any,
4545
context: dict[str, Any] | None = None,
4646
) -> dict[str, bool]:
4747
policy = self._policy_lookup(resource)
48-
return policy.grants(actor, resource, context)
48+
return await policy.grants(actor, resource, context)
4949

5050
def register(self, policy: policies.Policy[Any]):
5151
if hasattr(policy, "__orig_class__"):

entitled/policies.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Grouping of authorization rules around a particular resource type"""
22

3+
import asyncio
4+
35
from typing import Any, Callable, Generic, TypeVar
46

57
from entitled import exceptions
@@ -43,27 +45,27 @@ def __register(self, action: str, *rules: Rule[T]):
4345
if action not in self._registry:
4446
self._registry[action] = [*rules]
4547

46-
def grants(
48+
async def grants(
4749
self, actor: Any, resource: T | type[T], context: dict[str, Any] | None = None
4850
) -> dict[str, bool]:
4951
return {
50-
action: self.allows(action, actor, resource, context)
52+
action: await self.allows(action, actor, resource, context)
5153
for action in self._registry
5254
}
5355

54-
def allows(
56+
async def allows(
5557
self,
5658
action: str,
5759
actor: Any,
5860
resource: T | type[T],
5961
context: dict[str, Any] | None = None,
6062
) -> bool:
6163
try:
62-
return self.authorize(action, actor, resource, context)
64+
return await self.authorize(action, actor, resource, context)
6365
except exceptions.AuthorizationException:
6466
return False
6567

66-
def authorize(
68+
async def authorize(
6769
self,
6870
action: str,
6971
actor: Any,
@@ -75,7 +77,13 @@ def authorize(
7577
f"Action <{action}> undefined for this policy"
7678
)
7779

78-
if not any(rule(actor, resource, context) for rule in self._registry[action]):
80+
if not any(
81+
(
82+
await asyncio.gather(
83+
*(rule(actor, resource, context) for rule in self._registry[action])
84+
)
85+
)
86+
):
7987
raise exceptions.AuthorizationException("Unauthorized")
8088

8189
return True

entitled/rules.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
Actors represents any entity that, in an application, can act upon a resource.
88
"""
99

10-
from typing import Any, Callable, ClassVar, Generic, Protocol, TypeVar
10+
import inspect
11+
from typing import Any, Callable, ClassVar, Generic, Protocol, TypeVar, cast
1112

1213
from entitled import exceptions
1314

1415
Resource = TypeVar("Resource", contravariant=True)
1516

1617

17-
class RuleProtocol(Protocol[Resource]):
18+
class SyncRule(Protocol[Resource]):
1819
"""Defines valid functions for rules"""
1920

2021
def __call__(
@@ -25,6 +26,34 @@ def __call__(
2526
) -> bool: ...
2627

2728

29+
class AsyncRule(Protocol[Resource]):
30+
"""Defines valid functions for rules"""
31+
32+
async def __call__(
33+
self,
34+
actor: Any,
35+
resource: Resource | type[Resource],
36+
context: dict[str, Any] | None = None,
37+
) -> bool: ...
38+
39+
40+
RuleProtocol = SyncRule[Resource] | AsyncRule[Resource]
41+
42+
43+
async def handle_rule(
44+
func: AsyncRule[Resource] | SyncRule[Resource],
45+
actor: Any,
46+
resource: Resource | type[Resource],
47+
context: dict[str, Any] | None = None,
48+
):
49+
if inspect.iscoroutinefunction(func):
50+
fn = cast(AsyncRule[Resource], func)
51+
return await fn(actor, resource, context)
52+
else:
53+
fn = cast(SyncRule[Resource], func)
54+
return fn(actor, resource, context)
55+
56+
2857
class Rule(Generic[Resource]):
2958
"""Base class for rules
3059
@@ -55,33 +84,33 @@ def __register(self) -> None:
5584

5685
Rule._registry[self.name] = self
5786

58-
def __call__(
87+
async def __call__(
5988
self,
6089
actor: Any,
6190
resource: Resource | type[Resource],
6291
context: dict[str, Any] | None = None,
6392
) -> bool:
6493
if not context:
6594
context = {}
66-
return self.rule(actor, resource, context)
95+
return await handle_rule(self.rule, actor, resource, context)
6796

68-
def authorize(
97+
async def authorize(
6998
self,
7099
actor: Any,
71100
resource: Resource | type[Resource],
72101
context: dict[str, Any] | None = None,
73102
) -> bool:
74-
if not self(actor, resource, context):
103+
if not await self(actor, resource, context):
75104
raise exceptions.AuthorizationException("Unauthorized")
76105
return True
77106

78-
def allows(
107+
async def allows(
79108
self,
80109
actor: Any,
81110
resource: Resource | type[Resource],
82111
context: dict[str, Any] | None = None,
83112
) -> bool:
84-
return self(actor, resource, context)
113+
return await self(actor, resource, context)
85114

86115

87116
def rule(name: str) -> Callable[[RuleProtocol[Resource]], RuleProtocol[Resource]]:

entitled/rulesv1.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from typing import Any, ClassVar, Protocol, TypeVar
2+
3+
4+
Actor = TypeVar("Actor", contravariant=True)
5+
6+
7+
class RuleProto(Protocol[Actor]):
8+
async def __call__(self, actor: Actor, *args: Any, **kwargs: Any) -> bool: ...
9+
10+
11+
class Rule[Actor]:
12+
_registry: ClassVar[dict[str, RuleProto[Any]]] = {}
13+
_before_callback: RuleProto[Any] | None = None
14+
15+
@classmethod
16+
def define(cls, name: str, closure: RuleProto[Actor]) -> None:
17+
cls._registry[name] = closure
18+
19+
@classmethod
20+
def before(cls, rule: RuleProto[Actor] | None):
21+
cls._before_callback = rule
22+
23+
@classmethod
24+
async def allows(
25+
cls,
26+
name: str,
27+
actor: Actor,
28+
*args: Any,
29+
**kwargs: Any,
30+
) -> bool:
31+
if name in cls._registry:
32+
return await cls._registry[name](actor, args, kwargs)
33+
34+
return False
35+
36+
@classmethod
37+
async def denies(
38+
cls,
39+
name: str,
40+
actor: Actor,
41+
*args: Any,
42+
**kwargs: Any,
43+
) -> bool:
44+
return not await cls.allows(name, actor)
45+
46+
@classmethod
47+
async def any(
48+
cls,
49+
names: list[str],
50+
actor: Actor,
51+
*args: Any,
52+
**kwargs: Any,
53+
) -> bool:
54+
return any([await cls.allows(name, actor, args, kwargs) for name in names])
55+
56+
@classmethod
57+
async def none(
58+
cls,
59+
names: list[str],
60+
actor: Actor,
61+
*args: Any,
62+
**kwargs: Any,
63+
) -> bool:
64+
return not await cls.any(names, actor, args, kwargs)

tests/fixtures/policies.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
1+
from typing import Any
12
from entitled.policies import Policy
23
from tests.fixtures.models import Resource, Tenant, User
34

45
tenant_policy = Policy[Tenant]("tenant")
56

67

78
@tenant_policy.rule("member")
8-
def is_member(actor: User, resource: Tenant, context: dict | None = None) -> bool:
9+
def is_member(
10+
actor: User,
11+
resource: Tenant | type[Tenant],
12+
context: dict[str, Any] | None = None,
13+
) -> bool:
914
return actor.tenant == resource
1015

1116

1217
@tenant_policy.rule("admin_role")
13-
def has_admin_role(actor: User, resource: Tenant, context: dict | None = None) -> bool:
18+
def has_admin_role(
19+
actor: User,
20+
resource: Tenant | type[Tenant],
21+
context: dict[str, Any] | None = None,
22+
) -> bool:
1423
return is_member(actor, resource) and "admin" in actor.roles
1524

1625

1726
resource_policy = Policy[Resource]("node")
1827

1928

2029
@resource_policy.rule("edit")
21-
def can_edit(actor: User, resource: Resource, context: dict | None = None) -> bool:
22-
return resource.owner == actor or has_admin_role(actor, resource.tenant)
30+
def can_edit(
31+
actor: User,
32+
resource: Resource | type[Resource],
33+
context: dict[str, Any] | None = None,
34+
) -> bool:
35+
return has_admin_role(actor, resource.tenant) or resource.owner == actor
2336

2437

2538
@resource_policy.rule("view")
26-
def can_view(actor: User, resource: Resource, context: dict | None = None) -> bool:
39+
def can_view(
40+
actor: User,
41+
resource: Resource | type[Resource],
42+
context: dict[str, Any] | None = None,
43+
) -> bool:
2744
return is_member(actor, resource.tenant)

0 commit comments

Comments
 (0)