Skip to content

Commit 2714dac

Browse files
author
Mathias Bigaignon
committed
More tests, some fixes and quick refactors
1 parent 49d1fbd commit 2714dac

File tree

7 files changed

+117
-15
lines changed

7 files changed

+117
-15
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ __pycache__
33
*.pyc
44
.python-version
55
poetry.lock
6+
.coverage

entitled/client.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@
1111
class Client:
1212
"The Client class for decision-making centralization."
1313

14-
def __init__(self, base_path="policies"):
14+
def __init__(self, base_path: str | None = None):
1515
self._policy_registrar: dict[typing.Type, policies.Policy] = {}
16-
self._load_path = pathlib.Path(base_path)
17-
self.load_policies_from_path(self._load_path)
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)
1820

19-
def authorize(self, actor, action, resource, context: dict | None = None):
21+
def authorize(self, action, actor, resource, context: dict | None = None):
2022
policy = self._policy_lookup(resource)
21-
return policy.authorize(actor, action, resource, context)
23+
return policy.authorize(action, actor, resource, context)
2224

23-
def allows(self, actor, action, resource, context: dict | None = None) -> bool:
25+
def allows(self, action, actor, resource, context: dict | None = None) -> bool:
2426
policy = self._policy_lookup(resource)
25-
return policy.allows(actor, action, resource, context)
27+
return policy.allows(action, actor, resource, context)
2628

2729
def grants(self, actor, resource, context: dict | None = None):
2830
policy = self._policy_lookup(resource)
@@ -41,10 +43,12 @@ def register(self, policy: policies.Policy):
4143
raise AttributeError(f"Policy {policy} is incorrectly defined")
4244

4345
def reload_registrar(self):
44-
self.load_policies_from_path(self._load_path)
46+
if self._load_path is not None:
47+
self.load_policies_from_path(self._load_path)
4548

4649
def load_policies_from_path(self, path: pathlib.Path):
4750
for file_path in path.glob("*.py"):
51+
print(file_path)
4852
mod_name = file_path.stem
4953
full_module_name = ".".join(file_path.parts[:-1] + (mod_name,))
5054
spec = importlib.util.spec_from_file_location(full_module_name, file_path)

entitled/policies.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,26 +48,26 @@ def __register(self, action, *rules: Rule[T]):
4848
def grants(self, actor, resource: T, context: dict | None = None):
4949

5050
return filter(
51-
lambda action: self.allows(actor, action, resource, context),
51+
lambda action: self.allows(action, actor, resource, context),
5252
self._registry.keys(),
5353
)
5454

5555
def allows(
5656
self,
57-
actor,
5857
action,
58+
actor,
5959
resource: T,
6060
context: dict | None = None,
6161
) -> bool:
6262
try:
63-
return self.authorize(actor, action, resource, context)
63+
return self.authorize(action, actor, resource, context)
6464
except exceptions.AuthorizationException:
6565
return False
6666

6767
def authorize(
6868
self,
69-
actor,
7069
action,
70+
actor,
7171
resource: T,
7272
context: dict | None = None,
7373
) -> bool:

tests/fixtures/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ class User:
1414

1515

1616
@dataclasses.dataclass
17-
class Node:
17+
class Resource:
1818
name: str
1919
owner: User
2020
tenant: Tenant
21-
parent: "Node | None" = None

tests/fixtures/policies.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from entitled.policies import Policy
2+
from tests.fixtures.models import Resource, Tenant, User
3+
4+
tenant_policy = Policy[Tenant]("tenant")
5+
6+
7+
@tenant_policy.rule("member")
8+
def is_member(actor: User, resource: Tenant, context: dict | None = None) -> bool:
9+
return actor.tenant == resource
10+
11+
12+
@tenant_policy.rule("admin_role")
13+
def has_admin_role(actor: User, resource: Tenant, context: dict | None = None) -> bool:
14+
return is_member(actor, resource) and "admin" in actor.roles
15+
16+
17+
resource_policy = Policy[Resource]("node")
18+
19+
20+
@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)
23+
24+
25+
@resource_policy.rule("view")
26+
def can_view(actor: User, resource: Resource, context: dict | None = None) -> bool:
27+
return is_member(actor, resource.tenant)

tests/test_client.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
from _pytest.pytester import pytester
5+
6+
from entitled import exceptions
7+
from entitled.client import Client
8+
from entitled.rules import Rule
9+
from tests.fixtures.models import Resource, Tenant, User
10+
from tests.fixtures.policies import resource_policy, tenant_policy
11+
12+
13+
class TestClientCreation:
14+
def teardown_method(self):
15+
Rule.clear_registry()
16+
17+
def test_create_client(self):
18+
client = Client()
19+
assert client._load_path is None
20+
assert client._policy_registrar == {}
21+
22+
def test_create_client_with_loadpath(self):
23+
client = Client(base_path="tests/fixtures")
24+
25+
assert client._load_path == Path("tests/fixtures")
26+
assert len(client._policy_registrar.items()) == 2
27+
28+
29+
class TestClientDecision:
30+
31+
def teardown_method(self):
32+
Rule.clear_registry()
33+
34+
def test_grants(self):
35+
client = Client(base_path="tests/fixtures")
36+
tenant1 = Tenant("tenant1")
37+
tenant2 = Tenant("tenant2")
38+
user1 = User("user1", tenant1, set(["user"]))
39+
user2 = User("user2", tenant2, set(["user"]))
40+
resource1 = Resource("R1", user1, tenant1)
41+
42+
u1_grants = [g for g in client.grants(user1, resource1)]
43+
u2_grants = [g for g in client.grants(user2, resource1)]
44+
assert "view" in u1_grants and "edit" in u1_grants
45+
assert "view" not in u2_grants and "edit" not in u2_grants
46+
47+
def test_allows(self):
48+
client = Client(base_path="tests/fixtures")
49+
tenant1 = Tenant("tenant1")
50+
tenant2 = Tenant("tenant2")
51+
user1 = User("user1", tenant1, set(["user"]))
52+
user2 = User("user2", tenant2, set(["user"]))
53+
resource1 = Resource("R1", user1, tenant1)
54+
55+
assert client.allows("edit", user1, resource1)
56+
assert not client.allows("edit", user2, resource1)
57+
assert not client.allows("move", user2, resource1)
58+
59+
def test_authorize(self):
60+
client = Client(base_path="tests/fixtures")
61+
tenant1 = Tenant("tenant1")
62+
tenant2 = Tenant("tenant2")
63+
user1 = User("user1", tenant1, set(["user"]))
64+
user2 = User("user2", tenant2, set(["user"]))
65+
resource1 = Resource("R1", user1, tenant1)
66+
67+
assert client.authorize("edit", user1, resource1)
68+
with pytest.raises(exceptions.AuthorizationException):
69+
client.authorize("edit", user2, resource1)
70+
with pytest.raises(exceptions.UndefinedAction):
71+
client.authorize("move", user2, resource1)

tests/test_rules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_authorize(self):
9393

9494
new_tenant = Tenant("other_tenant")
9595
with pytest.raises(exceptions.AuthorizationException):
96-
assert res.authorize(test_user, new_tenant)
96+
res.authorize(test_user, new_tenant)
9797

9898
def test_allows(self):
9999
tenant = Tenant("test_tenant")

0 commit comments

Comments
 (0)