policy-pattern 0.1.0__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.
- policy_pattern/__init__.py +0 -0
- policy_pattern/adapters/__init__.py +3 -0
- policy_pattern/adapters/memory.py +135 -0
- policy_pattern/errors/__init__.py +19 -0
- policy_pattern/errors/base_error.py +28 -0
- policy_pattern/errors/evaluation_error.py +16 -0
- policy_pattern/errors/identifier_error.py +15 -0
- policy_pattern/errors/model_validation_error.py +15 -0
- policy_pattern/errors/mutation_error.py +15 -0
- policy_pattern/errors/relation_error.py +15 -0
- policy_pattern/errors/unknown_name_error.py +16 -0
- policy_pattern/errors/unknown_type_error.py +16 -0
- policy_pattern/evaluator.py +285 -0
- policy_pattern/models/__init__.py +34 -0
- policy_pattern/models/_schema_validation.py +130 -0
- policy_pattern/models/compiled_model.py +43 -0
- policy_pattern/models/direct_subject.py +47 -0
- policy_pattern/models/identifier.py +55 -0
- policy_pattern/models/model.py +46 -0
- policy_pattern/models/mutation.py +52 -0
- policy_pattern/models/object_reference.py +42 -0
- policy_pattern/models/operators.py +116 -0
- policy_pattern/models/reference.py +36 -0
- policy_pattern/models/relation.py +44 -0
- policy_pattern/models/subject.py +22 -0
- policy_pattern/models/subject_set.py +56 -0
- policy_pattern/models/tuple.py +52 -0
- policy_pattern/models/tuple_to_userset.py +51 -0
- policy_pattern/models/type.py +71 -0
- policy_pattern/ports/__init__.py +7 -0
- policy_pattern/ports/tuple_store.py +118 -0
- policy_pattern/py.typed +0 -0
- policy_pattern-0.1.0.dist-info/METADATA +148 -0
- policy_pattern-0.1.0.dist-info/RECORD +36 -0
- policy_pattern-0.1.0.dist-info/WHEEL +4 -0
- policy_pattern-0.1.0.dist-info/licenses/LICENSE.md +201 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Value Object representing a set of subjects in the authorization graph.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from .identifier import Identifier
|
|
9
|
+
from .object_reference import ObjectReference
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class SubjectSet:
|
|
14
|
+
"""
|
|
15
|
+
Value Object representing a set of subjects.
|
|
16
|
+
|
|
17
|
+
A subject set is a reference to *a relation on an object* — not a
|
|
18
|
+
single entity like `DirectSubject`/`ObjectReference`, but the set of
|
|
19
|
+
whoever holds that relation. `group:eng#member` refers to every
|
|
20
|
+
current member of `group:eng`, resolved at evaluation time, not a
|
|
21
|
+
fixed list stored here.
|
|
22
|
+
|
|
23
|
+
This is the other half of `Subject` (`Subject = DirectSubject |
|
|
24
|
+
SubjectSet`): `DirectSubject` names one entity;
|
|
25
|
+
`SubjectSet` names a computation over the graph ("whoever satisfies
|
|
26
|
+
this relation on this object right now"). It is exactly how nested
|
|
27
|
+
groups work in Zanzibar — `group:eng#member@group:backend#member`
|
|
28
|
+
makes every member of `group:backend` a member of `group:eng`,
|
|
29
|
+
without ever copying a membership list.
|
|
30
|
+
|
|
31
|
+
`relation` delegates to `Identifier` like every other identifier-shaped
|
|
32
|
+
field in this model (`ObjectReference.type`/`.id`,
|
|
33
|
+
`Relation.allowed_subject_types` entries) — no separate bounds/charset
|
|
34
|
+
logic, no separate error class needed for it.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
SubjectSet(object=ObjectReference(type="group", id="eng"), relation="member")
|
|
38
|
+
|
|
39
|
+
`object` is validated by `ObjectReference` itself; `relation` is
|
|
40
|
+
validated as an `Identifier` with `field="relation"`.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
InvalidIdentifierError: Propagated from `Identifier` if `relation` is invalid,
|
|
44
|
+
or from `ObjectReference` if `type`/`id` is invalid.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
object: ObjectReference
|
|
48
|
+
relation: str
|
|
49
|
+
|
|
50
|
+
_RELATION_IDENTIFIER_FIELD: ClassVar[str] = "relation"
|
|
51
|
+
|
|
52
|
+
def __post_init__(self) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Validates the subject set after initialization by delegating to Identifier.
|
|
55
|
+
"""
|
|
56
|
+
Identifier(value=self.relation, field=self._RELATION_IDENTIFIER_FIELD)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Value Object representing a stored relationship fact in the authorization graph.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .identifier import Identifier
|
|
8
|
+
from .object_reference import ObjectReference
|
|
9
|
+
from .subject import Subject
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class Tuple:
|
|
14
|
+
"""
|
|
15
|
+
Value Object representing one stored fact: `object#relation@subject`.
|
|
16
|
+
|
|
17
|
+
e.g. `document:42#owner@user:john` is
|
|
18
|
+
`Tuple(subject=DirectSubject(object=ObjectReference(type="user", id="john")),
|
|
19
|
+
relation="owner", object=ObjectReference(type="document", id="42"))`.
|
|
20
|
+
|
|
21
|
+
`relation` only names a relation here — it is validated as an
|
|
22
|
+
`Identifier` (non-empty, bounded, printable), but *not* checked against
|
|
23
|
+
an actual `Relation`/`Permission` declaration. `Tuple` has no schema
|
|
24
|
+
context to check that against; confirming `relation` actually names a
|
|
25
|
+
storable `Relation` (never a `Permission`) is `compile()`'s job on the
|
|
26
|
+
`Model`, not this class's.
|
|
27
|
+
|
|
28
|
+
`subject`/`object` are validated by `Subject`'s variants
|
|
29
|
+
(`DirectSubject`/`SubjectSet`) and `ObjectReference` themselves —
|
|
30
|
+
`Tuple` only validates the one field it owns directly.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
Tuple(
|
|
34
|
+
subject=DirectSubject(object=ObjectReference(type="user", id="john")),
|
|
35
|
+
relation="owner",
|
|
36
|
+
object=ObjectReference(type="document", id="42"),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
InvalidIdentifierError: If `relation` is invalid, or propagated from
|
|
41
|
+
`subject`/`object` construction.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
subject: Subject
|
|
45
|
+
relation: str
|
|
46
|
+
object: ObjectReference
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Validates the tuple after initialization by delegating relation to Identifier.
|
|
51
|
+
"""
|
|
52
|
+
Identifier(value=self.relation, field="relation")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Value Object representing a computed permission expression that traverses to a
|
|
3
|
+
related object via a relation and evaluates a computed permission there.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import ClassVar
|
|
8
|
+
|
|
9
|
+
from .identifier import Identifier
|
|
10
|
+
from .operators import PermissionExpression
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class TupleToUserset(PermissionExpression):
|
|
15
|
+
"""
|
|
16
|
+
Traverses to a related object via `tupleset`, then evaluates `computed` there.
|
|
17
|
+
|
|
18
|
+
This is the one node that is not a set operation on *this* object — it
|
|
19
|
+
jumps to a *different* object first. `document:42#parent@folder:eng`
|
|
20
|
+
plus `TupleToUserset(tupleset="parent", computed="viewer")` reads as
|
|
21
|
+
"find the object(s) related to this one via `parent`, then check
|
|
22
|
+
`viewer` on *that* object instead." It is how folder hierarchies grant
|
|
23
|
+
access to every document underneath without copying a permission onto
|
|
24
|
+
each child individually, and how nested groups
|
|
25
|
+
(`group:eng#member@group:backend#member`) work without flattening
|
|
26
|
+
membership into a stored list.
|
|
27
|
+
|
|
28
|
+
`tupleset` must resolve to a `Relation`, never a `Permission` —
|
|
29
|
+
traversal needs an actual stored edge to follow (`document:42#parent`
|
|
30
|
+
has to exist as data); there is nothing to traverse if `parent` were
|
|
31
|
+
itself a computed rule. `computed` has no such restriction: it is
|
|
32
|
+
evaluated on the object found via `tupleset`, and can be either a
|
|
33
|
+
`Relation` or another `Permission` there, resolved the same way
|
|
34
|
+
`Reference` resolves its own `name`.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
TupleToUserset(tupleset="parent", computed="viewer")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
tupleset: str
|
|
41
|
+
computed: str
|
|
42
|
+
|
|
43
|
+
_FIELD_TUPLESET_VALUE: ClassVar[str] = "tupleset"
|
|
44
|
+
_FIELD_COMPUTED_VALUE: ClassVar[str] = "computed"
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Validates `tupleset` and `computed` by delegating to `Identifier`.
|
|
49
|
+
"""
|
|
50
|
+
Identifier(value=self.tupleset, field=self._FIELD_TUPLESET_VALUE)
|
|
51
|
+
Identifier(value=self.computed, field=self._FIELD_COMPUTED_VALUE)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Value Object representing a declaration mutable at authoring time, but immutable at evaluation time.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from ..errors import InvalidIdentifierError
|
|
11
|
+
from .identifier import Identifier
|
|
12
|
+
from .operators import PermissionExpression
|
|
13
|
+
from .relation import Relation
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class Type:
|
|
18
|
+
"""
|
|
19
|
+
Value Object representing a type declaration in the authorization graph.
|
|
20
|
+
|
|
21
|
+
A `Type` declares a kind of object, along with the relations and
|
|
22
|
+
permissions it supports.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
Type(name="document", relations={}, permissions={})
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
InvalidIdentifierError: Propagated from `Identifier` if `name`, a
|
|
29
|
+
relation name, or a permission name is invalid, or if `name`
|
|
30
|
+
contains '#' (reserved for userset notation).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
relations: Mapping[str, Relation]
|
|
35
|
+
permissions: Mapping[str, PermissionExpression]
|
|
36
|
+
|
|
37
|
+
_TYPE_NAME_FIELD: ClassVar[str] = "type"
|
|
38
|
+
_RELATION_NAME_FIELD: ClassVar[str] = "relation"
|
|
39
|
+
_PERMISSION_NAME_FIELD: ClassVar[str] = "permission"
|
|
40
|
+
|
|
41
|
+
def __post_init__(self) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Validate `name` and every relation/permission name, then make the mappings read-only views.
|
|
44
|
+
|
|
45
|
+
`relations`/`permissions` keys are the actual schema identifiers —
|
|
46
|
+
`Reference`/`tupleset` names resolve against them — so they need the
|
|
47
|
+
same `Identifier` validation as `name` itself, not just the values.
|
|
48
|
+
|
|
49
|
+
`frozen=True` only blocks reassigning `relations`/`permissions`
|
|
50
|
+
themselves — a plain `dict` field would still be mutable in place
|
|
51
|
+
(`type_.relations["x"] = y`). Wrapping a copy in `MappingProxyType`
|
|
52
|
+
closes that gap: neither the returned view nor the caller's
|
|
53
|
+
original dict (once copied) can affect this instance afterward.
|
|
54
|
+
"""
|
|
55
|
+
Identifier(value=self.name, field=self._TYPE_NAME_FIELD)
|
|
56
|
+
if "#" in self.name:
|
|
57
|
+
# Reserved for userset notation ("type#relation") inside
|
|
58
|
+
# Relation.allowed_subject_types — _schema_validation.py tells a
|
|
59
|
+
# plain type-name entry apart from a userset one by checking for
|
|
60
|
+
# "#" in the string. If a real Type name could contain "#" too,
|
|
61
|
+
# that check couldn't tell them apart.
|
|
62
|
+
raise InvalidIdentifierError(
|
|
63
|
+
message=f"type name {self.name!r} cannot contain '#' — reserved for userset notation",
|
|
64
|
+
related=f"field={self._TYPE_NAME_FIELD!r}",
|
|
65
|
+
)
|
|
66
|
+
for relation_name in self.relations:
|
|
67
|
+
Identifier(value=relation_name, field=self._RELATION_NAME_FIELD)
|
|
68
|
+
for permission_name in self.permissions:
|
|
69
|
+
Identifier(value=permission_name, field=self._PERMISSION_NAME_FIELD)
|
|
70
|
+
object.__setattr__(self, "relations", MappingProxyType(dict(self.relations)))
|
|
71
|
+
object.__setattr__(self, "permissions", MappingProxyType(dict(self.permissions)))
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage port: TupleStore/View abstractions and the bounded-read result type.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from contextlib import AbstractContextManager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from ..models import Mutation, ObjectReference, Subject, Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class ReadResult:
|
|
14
|
+
"""
|
|
15
|
+
Result of a bounded `View.read_relation()` call.
|
|
16
|
+
|
|
17
|
+
`docs/semantics.md` requires that a truncated enumeration never be
|
|
18
|
+
representable as a complete one — `complete` carries that fact in the
|
|
19
|
+
type itself rather than a convention the caller has to already know
|
|
20
|
+
(e.g. a `limit + 1` sentinel). `tuples` is a `frozenset` — it carries no
|
|
21
|
+
order of its own. When more matching tuples exist than the caller's
|
|
22
|
+
`limit` allowed, `tuples` holds at most `limit` of them, deterministically
|
|
23
|
+
selected by the adapter's own canonical ranking (never arbitrary or
|
|
24
|
+
hash-order-dependent — see `MemoryTupleStore.read_relation`), and
|
|
25
|
+
`complete` is `False`; otherwise `tuples` holds all matches and
|
|
26
|
+
`complete` is `True`.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
ReadResult(tuples=frozenset({...}), complete=True)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
tuples: frozenset[Tuple]
|
|
33
|
+
complete: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TupleStore(ABC):
|
|
37
|
+
"""
|
|
38
|
+
Storage port for authorization tuples — mutations plus consistent-snapshot reads.
|
|
39
|
+
|
|
40
|
+
Deliberately schema-agnostic: this class stores and compares
|
|
41
|
+
structurally well-formed `(subject, relation, object)` triples and
|
|
42
|
+
knows nothing about `Relation` vs `Permission`, rewrite rules, or
|
|
43
|
+
`compile()` — that separation is what keeps a `PostgresTupleStore`
|
|
44
|
+
substitutable for `MemoryTupleStore` without either one leaking into
|
|
45
|
+
the rewrite engine (`docs/architecture.md`).
|
|
46
|
+
|
|
47
|
+
`write_tuples()` is the only mutating method an adapter must implement.
|
|
48
|
+
`delete_tuples()` is concrete, not abstract — a pure deletion is already
|
|
49
|
+
representable as `Mutation(additions=frozenset(), deletions=tuples)`,
|
|
50
|
+
so every adapter gets deletion for free instead of reimplementing the
|
|
51
|
+
same atomicity/disjointness guarantees `write_tuples()` already provides.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
store.write_tuples(Mutation(additions=frozenset({t}), deletions=frozenset()))
|
|
55
|
+
store.delete_tuples(frozenset({t}))
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def write_tuples(self, mutation: Mutation) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Atomically applies a mutation's additions and deletions.
|
|
62
|
+
|
|
63
|
+
See `docs/semantics.md` § Write mutations for the full contract
|
|
64
|
+
(atomicity, idempotency, visibility point) — this method's
|
|
65
|
+
implementation must satisfy it, this docstring does not restate it.
|
|
66
|
+
"""
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
def delete_tuples(self, tuples: frozenset[Tuple]) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Deletes the given tuples atomically, as a deletion-only Mutation.
|
|
72
|
+
"""
|
|
73
|
+
self.write_tuples(Mutation(additions=frozenset(), deletions=tuples))
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def snapshot(self) -> AbstractContextManager["View"]:
|
|
77
|
+
"""
|
|
78
|
+
Opens an evaluation-scoped, read-only view pinned to one consistent snapshot.
|
|
79
|
+
|
|
80
|
+
See `docs/semantics.md` § Snapshot consistency of check() — every
|
|
81
|
+
read issued through the same `View` must observe the same logical
|
|
82
|
+
store state for the view's whole lifetime.
|
|
83
|
+
"""
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class View(ABC):
|
|
88
|
+
"""
|
|
89
|
+
Read-only handle pinned to one consistent snapshot of a `TupleStore`.
|
|
90
|
+
|
|
91
|
+
No write method exists here at all — `docs/semantics.md` already
|
|
92
|
+
decided mutations are issued against `TupleStore` itself, never through
|
|
93
|
+
a view; this class makes that an actual absence of a method, not just a
|
|
94
|
+
sentence in a doc.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
with store.snapshot() as view:
|
|
98
|
+
view.read_tuple(subject, "owner", object)
|
|
99
|
+
view.read_relation(object, "owner", limit=100)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def read_tuple(self, subject: Subject, relation: str, object: ObjectReference) -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Returns whether the exact tuple `(subject, relation, object)` exists in this view.
|
|
106
|
+
"""
|
|
107
|
+
raise NotImplementedError
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def read_relation(self, object: ObjectReference, relation: str, limit: int) -> ReadResult:
|
|
111
|
+
"""
|
|
112
|
+
Enumerates subjects/usersets holding `relation` on `object`, bounded by `limit`.
|
|
113
|
+
|
|
114
|
+
See `ReadResult`'s docstring for what `complete=False` means and
|
|
115
|
+
when it is set. An adapter must not materialize more than
|
|
116
|
+
`limit + 1` matching tuples before returning — that would violate the contract.
|
|
117
|
+
"""
|
|
118
|
+
raise NotImplementedError
|
policy_pattern/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: policy-pattern
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal, embeddable ReBAC authorization engine for Python, inspired by Google Zanzibar.
|
|
5
|
+
Keywords: python,authorization,access-control,rebac,zanzibar,security
|
|
6
|
+
Author: Guillem Pozo Sebastián
|
|
7
|
+
Author-email: Guillem Pozo Sebastián <58363308+guillem-ps@users.noreply.github.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Classifier: Topic :: Security
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Project-URL: Homepage, https://github.com/guillem-ps/policy-pattern
|
|
22
|
+
Project-URL: Repository, https://github.com/guillem-ps/policy-pattern
|
|
23
|
+
Project-URL: Issues, https://github.com/guillem-ps/policy-pattern/issues
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# policy-pattern
|
|
27
|
+
|
|
28
|
+
> A small, embeddable ReBAC authorization engine for Python, inspired by Google Zanzibar.
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://github.com/guillem-ps/policy-pattern/actions/workflows/test.yaml" target="_blank">
|
|
32
|
+
<img src="https://github.com/guillem-ps/policy-pattern/actions/workflows/test.yaml/badge.svg" alt="Test Pipeline">
|
|
33
|
+
</a>
|
|
34
|
+
<a href="https://github.com/guillem-ps/policy-pattern/actions/workflows/lint.yaml" target="_blank">
|
|
35
|
+
<img src="https://github.com/guillem-ps/policy-pattern/actions/workflows/lint.yaml/badge.svg" alt="Lint Pipeline">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://codecov.io/gh/guillem-ps/policy-pattern" target="_blank">
|
|
38
|
+
<img src="https://codecov.io/gh/guillem-ps/policy-pattern/graph/badge.svg" alt="Coverage Pipeline">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="pyproject.toml" target="_blank">
|
|
41
|
+
<img src="https://img.shields.io/badge/version-0.1.0-blue" alt="Package Version">
|
|
42
|
+
</a>
|
|
43
|
+
<a href="pyproject.toml" target="_blank">
|
|
44
|
+
<img src="https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue" alt="Supported Python Versions">
|
|
45
|
+
</a>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
`policy-pattern` is a standalone Python library for relationship-based access control (ReBAC). It models
|
|
49
|
+
authorization as relationships between subjects and objects and evaluates those relationships through composable
|
|
50
|
+
authorization rules.
|
|
51
|
+
|
|
52
|
+
- **No authorization server**
|
|
53
|
+
- **No required framework**
|
|
54
|
+
- **No required database**
|
|
55
|
+
- **No network hop**
|
|
56
|
+
|
|
57
|
+
The library is designed to be embedded directly into Python applications that need richer authorization semantics
|
|
58
|
+
than traditional role-based access control.
|
|
59
|
+
|
|
60
|
+
> **Status:** v1 is code-complete — model, `compile()`, the storage port, `MemoryTupleStore`, and the `Evaluator`
|
|
61
|
+
> all exist and are tested.
|
|
62
|
+
|
|
63
|
+
See [Architecture § Scope](docs/architecture.md#scope) for what's planned (v1.x/v2) and unscheduled ideas.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Table of Contents
|
|
68
|
+
|
|
69
|
+
* [Example](#example)
|
|
70
|
+
* [Documentation](#documentation)
|
|
71
|
+
* [Contributing](#contributing)
|
|
72
|
+
* [License](#license)
|
|
73
|
+
* [References](#references)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Example
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
model = Model()
|
|
81
|
+
model.types["document"] = Type(
|
|
82
|
+
name="document",
|
|
83
|
+
relations={
|
|
84
|
+
"owner": Relation(allowed_subject_types=("user",)),
|
|
85
|
+
"parent": Relation(allowed_subject_types=("folder",)),
|
|
86
|
+
},
|
|
87
|
+
permissions={
|
|
88
|
+
"viewer": Union(left=Reference(name="owner"), right=TupleToUserset(tupleset="parent", computed="viewer")),
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
compiled = model.compile()
|
|
92
|
+
|
|
93
|
+
evaluator = Evaluator(model=compiled, store=store)
|
|
94
|
+
evaluator.check(alice, "viewer", document_42)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`viewer` here means "the document's own owner, or the owner of whatever folder it lives in" — a document under
|
|
98
|
+
`folder:engineering` inherits access from that folder without copying a permission onto every document.
|
|
99
|
+
|
|
100
|
+
See the [Usage Guide](docs/usage/README.md) for the full, runnable walkthrough — every rewrite operator, nested
|
|
101
|
+
groups, `EvaluationBudget`, and error handling.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
The repository uses specifications and architecture documents as part of the implementation contract — they
|
|
108
|
+
describe **what must be true**, independent of any one adapter's implementation.
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
docs/
|
|
112
|
+
├── README.md — documentation index, start here
|
|
113
|
+
├── usage/README.md — worked examples for every rewrite operator, budgets, error handling
|
|
114
|
+
├── architecture.md — the model shape, compile(), the storage port, the evaluator, and scope/roadmap
|
|
115
|
+
├── semantics.md — the storage contract every TupleStore adapter must satisfy
|
|
116
|
+
└── errors.md — the PPxxx error code ranges and what each one means
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Start with the [Usage Guide](docs/usage/README.md) to see the library working end to end, then
|
|
120
|
+
[Architecture](docs/architecture.md) to understand why the model is shaped the way it is.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Contributing
|
|
125
|
+
|
|
126
|
+
We love community help! Before you open an issue or pull request, please read:
|
|
127
|
+
|
|
128
|
+
* [How to Contribute](.github/CONTRIBUTING.md)
|
|
129
|
+
* [Code of Conduct](.github/CODE_OF_CONDUCT.md)
|
|
130
|
+
* [Security Policy](SECURITY.md)
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
Licensed under the [Apache License 2.0](LICENSE.md).
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## References
|
|
141
|
+
|
|
142
|
+
The design is influenced by authorization systems and literature including:
|
|
143
|
+
|
|
144
|
+
* [Google Zanzibar](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/)
|
|
145
|
+
* [OpenFGA](https://openfga.dev)
|
|
146
|
+
* [SpiceDB](https://github.com/authzed/spicedb) / [AuthZed](https://authzed.com)
|
|
147
|
+
|
|
148
|
+
`policy-pattern` is an independent project and is not an implementation of, or affiliated with, those projects.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
policy_pattern/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
policy_pattern/adapters/__init__.py,sha256=J2GWAI4H1SoveZ9ocF6D2Cg-aozMtKlKV_pDKs2Jq3o,69
|
|
3
|
+
policy_pattern/adapters/memory.py,sha256=wFWt5P6QJxtqkz7onuYdCt8-ISz9FoVj77VvUb_8G0U,5027
|
|
4
|
+
policy_pattern/errors/__init__.py,sha256=iJye0Mnn6n6JcN630fTCv5bxhFw5syV3Hs4eu-FD2D0,603
|
|
5
|
+
policy_pattern/errors/base_error.py,sha256=aXuhBzC0s2vMPcVy8OTXYuS17Pjhz9eCYQag0FbCJcY,967
|
|
6
|
+
policy_pattern/errors/evaluation_error.py,sha256=nB1sD5uL5FYxwXFgA28tE38XnKuSSu8CdmUdGzowQZw,463
|
|
7
|
+
policy_pattern/errors/identifier_error.py,sha256=Rd8dhzIKKCjAm46wjsJIJA7AVsjs3NhWrlJZ9csqRpQ,320
|
|
8
|
+
policy_pattern/errors/model_validation_error.py,sha256=LavYcAyQpqHJKoaCtXG_TC0Qv9zmyCTAeCerWHVrvj8,339
|
|
9
|
+
policy_pattern/errors/mutation_error.py,sha256=kazfo30ma3ZZq1LvOcjZK4esMUzxphB4n6hrSL9hmjk,314
|
|
10
|
+
policy_pattern/errors/relation_error.py,sha256=kWV-yxr7I-EmtHL3xnEqrTrkYNNU44bGswhwoV56W9c,314
|
|
11
|
+
policy_pattern/errors/unknown_name_error.py,sha256=2jzHRzlQSF4K3OL9oS2aGlS08EiKBSIIEIbzpZfi1GU,445
|
|
12
|
+
policy_pattern/errors/unknown_type_error.py,sha256=huIxIJVM1p6ngqdzq7sN9CQXY7shmhvSunZsb09y9Wk,446
|
|
13
|
+
policy_pattern/evaluator.py,sha256=KmM4RSfZ4fV2Ft2IJ37Wpt5-me3JI-lhKwyDwiVmSgM,12211
|
|
14
|
+
policy_pattern/models/__init__.py,sha256=pGhe4AnuH4Em9Iz9lpGL2hflt-ko1OshLSmrvWNs0Tg,837
|
|
15
|
+
policy_pattern/models/_schema_validation.py,sha256=W712R6iBQ4rM0HvqUOyXpqy5X5ys2vKRMzqdKdscMYw,6140
|
|
16
|
+
policy_pattern/models/compiled_model.py,sha256=Q00lhNwGl1jLlEuqbc57MZIKs5haJdKgp_qLjSPq28w,1581
|
|
17
|
+
policy_pattern/models/direct_subject.py,sha256=Q-77hHX5Hk3VMZGuFDK6uQJIX3k2Yoe_LMGstAyupfU,1889
|
|
18
|
+
policy_pattern/models/identifier.py,sha256=Jyt2oXiX2fWQW31UPFJXVnfVq7wCnseJg0DrJeemDYQ,1747
|
|
19
|
+
policy_pattern/models/model.py,sha256=2PdajMGKppBeRRnzRjyIOR-S1lWzBLfAGAvkT15X8Ro,1739
|
|
20
|
+
policy_pattern/models/mutation.py,sha256=B5IbfaTdvwvmrBQtXrciBFfe5qHGt0DCOYRgC2zdiKY,1660
|
|
21
|
+
policy_pattern/models/object_reference.py,sha256=r7tErTsOkFZSJk3gHXMRL2txiv1cyQdPRpT_nkcOWDY,1240
|
|
22
|
+
policy_pattern/models/operators.py,sha256=tVVBwWloJ2mL9aKpqjZERBO2MjuAhT0Kv1FfUFcEgCI,4337
|
|
23
|
+
policy_pattern/models/reference.py,sha256=WFbZ8TVukEleDbIpS32lWNMYC6jfxqimiYDyuvbsTRo,1252
|
|
24
|
+
policy_pattern/models/relation.py,sha256=xGz4GxfboiRf1i3HJe-1bAR4iFMViVc656Ykob-iCro,1473
|
|
25
|
+
policy_pattern/models/subject.py,sha256=isIXEWD2j1YCCyCn4C46Ffa7QSSd0saQ6rmr-9Uuoo4,1052
|
|
26
|
+
policy_pattern/models/subject_set.py,sha256=lRrOyP_SWPbACxs9rityDa1P570qGvSlZDxon0xmHUA,2089
|
|
27
|
+
policy_pattern/models/tuple.py,sha256=ty5dcn9mBpWlZlJlVAcwG1MaGxIyq1MzeSJRvoWw_WA,1790
|
|
28
|
+
policy_pattern/models/tuple_to_userset.py,sha256=IMGlZG5tJOHai1sddxMIM-4TbJwFMdJ8xE2B0Y1wPC8,2055
|
|
29
|
+
policy_pattern/models/type.py,sha256=98cXOUOkTU1XEpLKWk18MS-Xzn0psWMDh8Jm6ykvKJA,3048
|
|
30
|
+
policy_pattern/ports/__init__.py,sha256=qAg-9xRvVCzP80kYf08R-Mk7GraLokQpg4WC28xhQiA,117
|
|
31
|
+
policy_pattern/ports/tuple_store.py,sha256=qzHIlgUQVc5bHZPbu0NWJXoD8w2NV9XPBtHwoAzTbKw,4619
|
|
32
|
+
policy_pattern/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
policy_pattern-0.1.0.dist-info/licenses/LICENSE.md,sha256=pBo98b3diIHXZ6w8GMvFkU9_m_uIrPzHvUu9-vqIbTI,11353
|
|
34
|
+
policy_pattern-0.1.0.dist-info/WHEEL,sha256=rp-k_VZaX8oABFEtLwG_Df95p8LSKt9-uPyQ4jKH0P4,81
|
|
35
|
+
policy_pattern-0.1.0.dist-info/METADATA,sha256=mgX-g3yVTFMjZvPUqeW-tZdHJzStp5aWltr4asCIao0,5687
|
|
36
|
+
policy_pattern-0.1.0.dist-info/RECORD,,
|