Skip to content

Commit f8d750a

Browse files
author
m-bigaignon
committed
WIP
1 parent 65e1273 commit f8d750a

22 files changed

+976
-654
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: 43 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,57 @@
1-
"""Centralization of all decision-making processes on a single decision point"""
1+
from typing import Any, Literal
22

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

107

118
class Client:
12-
"The Client class for decision-making centralization."
9+
def __init__(self):
10+
self._rule_registry: dict[str, Rule[Any]] = {}
1311

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)
12+
def define_rule(self, name: str, callable: RuleProto[Actor]) -> Rule[Actor]:
13+
rule = Rule(name, callable)
14+
self._rule_registry[rule.name] = rule
15+
return rule
2016

21-
async def authorize(
17+
async def inspect(
2218
self,
23-
action: str,
24-
actor: Any,
25-
resource: Any,
26-
context: dict[str, Any] | None = None,
27-
) -> bool:
28-
policy = self._policy_lookup(resource)
29-
return await policy.authorize(action, actor, resource, context)
19+
name: str,
20+
actor: Actor,
21+
*args: Any,
22+
**kwargs: Any,
23+
) -> Response:
24+
if name in self._rule_registry:
25+
return await self._rule_registry[name].inspect(actor, *args, **kwargs)
26+
return Err(f"No rule found with name '{name}'")
3027

3128
async def allows(
3229
self,
33-
action: str,
34-
actor: Any,
35-
resource: Any,
36-
context: dict[str, Any] | None = None,
30+
name: str,
31+
actor: Actor,
32+
*args: Any,
33+
**kwargs: Any,
3734
) -> bool:
38-
policy = self._policy_lookup(resource)
39-
return await policy.allows(action, actor, resource, context)
35+
if name in self._rule_registry:
36+
return await self._rule_registry[name].allows(actor, *args, **kwargs)
37+
return False
4038

41-
async def grants(
39+
async def denies(
4240
self,
43-
actor: Any,
44-
resource: Any,
45-
context: dict[str, Any] | None = None,
46-
) -> dict[str, bool]:
47-
policy = self._policy_lookup(resource)
48-
return await 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")
41+
name: str,
42+
actor: Actor,
43+
*args: Any,
44+
**kwargs: Any,
45+
) -> bool:
46+
return not self.allows(name, actor, *args, **kwargs)
9647

97-
return self._policy_registrar[lookup_key]
48+
async def authorize(
49+
self,
50+
name: str,
51+
actor: Actor,
52+
*args: Any,
53+
**kwargs: Any,
54+
) -> Literal[True]:
55+
if name in self._rule_registry:
56+
return await self._rule_registry[name].authorize(actor, *args, **kwargs)
57+
raise AuthorizationException(f"No rule found with name '{name}'")

entitled/old_client.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Centralization of all decision-making processes on a single decision point"""
2+
3+
import importlib.util
4+
import pathlib
5+
import types
6+
from typing import Any
7+
8+
from entitled import old_policies
9+
10+
11+
class Client:
12+
"The Client class for decision-making centralization."
13+
14+
def __init__(self, base_path: str | None = None):
15+
self._policy_registrar: dict[type, old_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+
21+
async def authorize(
22+
self,
23+
action: str,
24+
actor: Any,
25+
resource: Any,
26+
context: dict[str, Any] | None = None,
27+
) -> bool:
28+
policy = self._policy_lookup(resource)
29+
return await policy.authorize(action, actor, resource, context)
30+
31+
async def allows(
32+
self,
33+
action: str,
34+
actor: Any,
35+
resource: Any,
36+
context: dict[str, Any] | None = None,
37+
) -> bool:
38+
policy = self._policy_lookup(resource)
39+
return await policy.allows(action, actor, resource, context)
40+
41+
async def grants(
42+
self,
43+
actor: Any,
44+
resource: Any,
45+
context: dict[str, Any] | None = None,
46+
) -> dict[str, bool]:
47+
policy = self._policy_lookup(resource)
48+
return await policy.grants(actor, resource, context)
49+
50+
def register(self, policy: old_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, old_policies.Policy):
86+
try:
87+
self.register(attr)
88+
except (ValueError, AttributeError):
89+
pass
90+
91+
def _policy_lookup(self, resource: Any) -> old_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]

entitled/old_policies.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Grouping of authorization rules around a particular resource type"""
2+
3+
import asyncio
4+
5+
from typing import Any, Callable, Generic, TypeVar
6+
7+
from entitled import exceptions
8+
from entitled.old_rules import Rule, RuleProtocol
9+
10+
T = TypeVar("T")
11+
12+
13+
class Policy(Generic[T]):
14+
"""A grouping of rules refering the given resource type."""
15+
16+
def __init__(
17+
self,
18+
label: str | None = None,
19+
rules: dict[str, list[Rule[T]]] | None = None,
20+
):
21+
self._registry: dict[
22+
str,
23+
list[Rule[T]],
24+
] = {}
25+
self.label = label
26+
27+
if not rules:
28+
rules = {}
29+
30+
for action, rule in rules.items():
31+
self.__register(action, *rule)
32+
33+
def rule(self, name: str) -> Callable[[RuleProtocol[T]], RuleProtocol[T]]:
34+
def wrapped(func: RuleProtocol[T]):
35+
rule_name = name
36+
if self.label is not None:
37+
rule_name = self.label + ":" + rule_name
38+
new_rule = Rule[T](rule_name, func)
39+
self.__register(name, new_rule)
40+
return func
41+
42+
return wrapped
43+
44+
def __register(self, action: str, *rules: Rule[T]):
45+
if action not in self._registry:
46+
self._registry[action] = [*rules]
47+
48+
async def grants(
49+
self, actor: Any, resource: T | type[T], context: dict[str, Any] | None = None
50+
) -> dict[str, bool]:
51+
return {
52+
action: await self.allows(action, actor, resource, context)
53+
for action in self._registry
54+
}
55+
56+
async def allows(
57+
self,
58+
action: str,
59+
actor: Any,
60+
resource: T | type[T],
61+
context: dict[str, Any] | None = None,
62+
) -> bool:
63+
try:
64+
return await self.authorize(action, actor, resource, context)
65+
except exceptions.AuthorizationException:
66+
return False
67+
68+
async def authorize(
69+
self,
70+
action: str,
71+
actor: Any,
72+
resource: T | type[T],
73+
context: dict[str, Any] | None = None,
74+
) -> bool:
75+
if action not in self._registry:
76+
raise exceptions.UndefinedAction(
77+
f"Action <{action}> undefined for this policy"
78+
)
79+
80+
if not any(
81+
(
82+
await asyncio.gather(
83+
*(rule(actor, resource, context) for rule in self._registry[action])
84+
)
85+
)
86+
):
87+
raise exceptions.AuthorizationException("Unauthorized")
88+
89+
return True

0 commit comments

Comments
 (0)