Skip to content

Commit e1eaeaf

Browse files
author
Mathias BIGAIGNON
committed
Squash merge v1.0 into main
1 parent 6ccede0 commit e1eaeaf

File tree

18 files changed

+1077
-708
lines changed

18 files changed

+1077
-708
lines changed

entitled/__init__.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +0,0 @@
1-
"""Importing some base classes at package root level for convenience"""
2-
3-
from entitled.client import Client
4-
from entitled.policies import Policy
5-
from entitled.rules import Rule
6-
7-
__all__ = ["Rule", "Policy", "Client"]

entitled/client.py

Lines changed: 65 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,85 @@
1-
"""Centralization of all decision-making processes on a single decision point"""
1+
import re
2+
from typing import Any, Literal
23

3-
import importlib.util
4-
import pathlib
5-
import types
6-
from typing import Any
7-
8-
from entitled import policies
4+
from entitled.exceptions import AuthorizationException
5+
from entitled.policies import Policy
6+
from entitled.response import Err, Response
7+
from entitled.rules import Actor, Rule, RuleProto
98

109

1110
class Client:
12-
"The Client class for decision-making centralization."
11+
def __init__(self):
12+
self._policy_registry: dict[type, Policy[Any]] = {}
13+
self._rule_registry: dict[str, Rule[Any]] = {}
14+
15+
def define_rule(self, name: str, callable: RuleProto[Actor]) -> Rule[Actor]:
16+
rule = Rule(name, callable)
17+
self._rule_registry[rule.name] = rule
18+
return rule
1319

14-
def __init__(self, base_path: str | None = None):
15-
self._policy_registrar: dict[type, policies.Policy[Any]] = {}
16-
self._load_path = None
17-
if base_path:
18-
self._load_path = pathlib.Path(base_path)
19-
self.load_policies_from_path(self._load_path)
20+
def register_policy(self, policy: Policy[Any]):
21+
resource_type = getattr(policy, "__orig_class__").__args__[0]
22+
self._policy_registry[resource_type] = policy
2023

21-
def authorize(
24+
def _resolve_policy(self, resource: Any) -> Policy[Any] | None:
25+
lookup_key = resource if isinstance(resource, type) else type(resource)
26+
policy = self._policy_registry.get(lookup_key, None)
27+
return policy
28+
29+
async def inspect(
30+
self,
31+
name: str,
32+
actor: Any,
33+
resource: Any,
34+
*args: Any,
35+
**kwargs: Any,
36+
) -> Response:
37+
policy = self._resolve_policy(resource)
38+
if policy is not None:
39+
return await policy.inspect(name, actor, resource, *args, **kwargs)
40+
return Err(f"No policy found with name '{name}'")
41+
42+
async def allows(
2243
self,
23-
action: str,
44+
name: str,
2445
actor: Any,
2546
resource: Any,
26-
context: dict[str, Any] | None = None,
47+
*args: Any,
48+
**kwargs: Any,
2749
) -> bool:
28-
policy = self._policy_lookup(resource)
29-
return policy.authorize(action, actor, resource, context)
50+
return (await self.inspect(name, actor, resource, *args, **kwargs)).allowed()
3051

31-
def allows(
52+
async def denies(
3253
self,
33-
action: str,
54+
name: str,
3455
actor: Any,
3556
resource: Any,
36-
context: dict[str, Any] | None = None,
57+
*args: Any,
58+
**kwargs: Any,
3759
) -> bool:
38-
policy = self._policy_lookup(resource)
39-
return policy.allows(action, actor, resource, context)
60+
return not await self.allows(name, actor, resource, *args, **kwargs)
4061

41-
def grants(
62+
async def authorize(
63+
self,
64+
name: str,
65+
actor: Any,
66+
resource: Any,
67+
*args: Any,
68+
**kwargs: Any,
69+
) -> Literal[True]:
70+
result = await self.inspect(name, actor, resource, *args, **kwargs)
71+
if not result.allowed():
72+
raise AuthorizationException(result.message())
73+
return True
74+
75+
async def grants(
4276
self,
4377
actor: Any,
4478
resource: Any,
45-
context: dict[str, Any] | None = None,
79+
*args: Any,
80+
**kwargs: Any,
4681
) -> dict[str, bool]:
47-
policy = self._policy_lookup(resource)
48-
return policy.grants(actor, resource, context)
49-
50-
def register(self, policy: policies.Policy[Any]):
51-
if hasattr(policy, "__orig_class__"):
52-
resource_type = getattr(policy, "__orig_class__").__args__[0]
53-
if resource_type not in self._policy_registrar:
54-
self._policy_registrar[resource_type] = policy
55-
else:
56-
raise ValueError(
57-
"A policy is already registered for this resource type"
58-
)
59-
else:
60-
raise AttributeError(f"Policy {policy} is incorrectly defined")
61-
62-
def reload_registrar(self):
63-
if self._load_path is not None:
64-
self.load_policies_from_path(self._load_path)
65-
66-
def load_policies_from_path(self, path: pathlib.Path):
67-
for file_path in path.glob("*.py"):
68-
print(file_path)
69-
mod_name = file_path.stem
70-
full_module_name = ".".join(file_path.parts[:-1] + (mod_name,))
71-
spec = importlib.util.spec_from_file_location(full_module_name, file_path)
72-
if spec:
73-
module = importlib.util.module_from_spec(spec)
74-
if spec.loader:
75-
try:
76-
spec.loader.exec_module(module)
77-
except Exception as e:
78-
raise e
79-
80-
self._register_from_module(module)
81-
82-
def _register_from_module(self, module: types.ModuleType):
83-
for attribute_name in dir(module):
84-
attr = getattr(module, attribute_name)
85-
if isinstance(attr, policies.Policy):
86-
try:
87-
self.register(attr)
88-
except (ValueError, AttributeError):
89-
pass
90-
91-
def _policy_lookup(self, resource: Any) -> policies.Policy[Any]:
92-
lookup_key = resource if isinstance(resource, type) else type(resource)
93-
94-
if lookup_key not in self._policy_registrar:
95-
raise ValueError("No policy registered for this resource type")
96-
97-
return self._policy_registrar[lookup_key]
82+
policy = self._resolve_policy(resource)
83+
if policy is None:
84+
return {}
85+
return await policy.grants(actor, resource, *args, **kwargs)

entitled/policies.py

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,93 @@
11
"""Grouping of authorization rules around a particular resource type"""
22

3-
from typing import Any, Callable, Generic, TypeVar
3+
from typing import Any, TypeVar
44

55
from entitled import exceptions
6-
from entitled.rules import Rule, RuleProtocol
6+
from entitled.response import Err, Ok, Response
7+
from entitled.rules import Rule, RuleProto
78

89
T = TypeVar("T")
910

1011

11-
class Policy(Generic[T]):
12+
class Policy[T]:
1213
"""A grouping of rules refering the given resource type."""
1314

15+
_registry: dict[str, Rule[Any]]
16+
1417
def __init__(
1518
self,
16-
label: str | None = None,
17-
rules: dict[str, list[Rule[T]]] | None = None,
19+
rules: dict[str, Rule[Any]] | None = None,
1820
):
19-
self._registry: dict[
20-
str,
21-
list[Rule[T]],
22-
] = {}
23-
self.label = label
21+
self._registry = {}
2422

2523
if not rules:
2624
rules = {}
2725

2826
for action, rule in rules.items():
29-
self.__register(action, *rule)
27+
self.register(action, *rule)
3028

31-
def rule(self, name: str) -> Callable[[RuleProtocol[T]], RuleProtocol[T]]:
32-
def wrapped(func: RuleProtocol[T]):
33-
rule_name = name
34-
if self.label is not None:
35-
rule_name = self.label + ":" + rule_name
36-
new_rule = Rule[T](rule_name, func)
37-
self.__register(name, new_rule)
38-
return func
29+
def rule(self, func: RuleProto[Any]):
30+
rule_name = f"{func.__name__}"
31+
new_rule = Rule[T](rule_name, func)
32+
self.register(rule_name, new_rule)
33+
return func
3934

40-
return wrapped
35+
def register(self, action: str, rule: Rule[T]):
36+
self._registry[action] = rule
4137

42-
def __register(self, action: str, *rules: Rule[T]):
38+
async def inspect(
39+
self,
40+
action: str,
41+
actor: Any,
42+
*args: Any,
43+
**kwargs: Any,
44+
) -> Response:
4345
if action not in self._registry:
44-
self._registry[action] = [*rules]
45-
46-
def grants(
47-
self, actor: Any, resource: T | type[T], context: dict[str, Any] | None = None
48-
) -> dict[str, bool]:
49-
return {
50-
action: self.allows(action, actor, resource, context)
51-
for action in self._registry
52-
}
46+
return Err(f"Action <{action}> undefined for this policy")
47+
return await self._registry[action].inspect(actor, *args, **kwargs)
5348

54-
def allows(
49+
async def allows(
5550
self,
5651
action: str,
5752
actor: Any,
58-
resource: T | type[T],
59-
context: dict[str, Any] | None = None,
53+
*args: Any,
54+
**kwargs: Any,
6055
) -> bool:
61-
try:
62-
return self.authorize(action, actor, resource, context)
63-
except exceptions.AuthorizationException:
64-
return False
56+
return (await self.inspect(action, actor, *args, **kwargs)).allowed()
6557

66-
def authorize(
58+
async def denies(
6759
self,
6860
action: str,
6961
actor: Any,
70-
resource: T | type[T],
71-
context: dict[str, Any] | None = None,
62+
*args: Any,
63+
**kwargs: Any,
7264
) -> bool:
73-
if action not in self._registry:
74-
raise exceptions.UndefinedAction(
75-
f"Action <{action}> undefined for this policy"
76-
)
77-
78-
if not any(rule(actor, resource, context) for rule in self._registry[action]):
79-
raise exceptions.AuthorizationException("Unauthorized")
65+
return not (await self.inspect(action, actor, *args, **kwargs)).allowed()
8066

67+
async def authorize(
68+
self,
69+
action: str,
70+
actor: Any,
71+
*args: Any,
72+
**kwargs: Any,
73+
) -> bool:
74+
res = await self.inspect(action, actor, *args, **kwargs)
75+
if not res.allowed():
76+
raise exceptions.AuthorizationException(res.message())
8177
return True
78+
79+
async def grants(
80+
self,
81+
actor: Any,
82+
*args: Any,
83+
**kwargs: Any,
84+
) -> dict[str, bool]:
85+
return {
86+
action: await self.allows(
87+
action,
88+
actor,
89+
*args,
90+
**kwargs,
91+
)
92+
for action in self._registry
93+
}

entitled/response.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from dataclasses import dataclass
2+
from typing import TypeVar
3+
4+
T = TypeVar("T")
5+
6+
7+
@dataclass(frozen=True)
8+
class Ok:
9+
def allowed(self):
10+
return True
11+
12+
def message(self):
13+
return None
14+
15+
16+
@dataclass(frozen=True)
17+
class Err:
18+
msg: str = ""
19+
20+
def allowed(self):
21+
return False
22+
23+
def message(self):
24+
return self.msg
25+
26+
27+
Response = Ok | Err

0 commit comments

Comments
 (0)