tba-durability-authorization-policy 0.4.0.dev2__tar.gz

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.
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: tba-durability-authorization-policy
3
+ Version: 0.4.0.dev2
4
+ Summary: Carrier-neutral layer-30 durable table operations.
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.10,<3.15
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: tba-bases-replay (==0.4.0.dev2)
14
+ Requires-Dist: tba-contracts-authorization (==0.4.0.dev3)
15
+ Requires-Dist: tba-contracts-identity (==0.4.0.dev2)
16
+ Requires-Dist: tba-contracts-security-trust (==0.1.0)
17
+ Requires-Dist: tba-primitives-identity (==0.4.0.dev2)
18
+ Requires-Dist: tba-storage-authorization-policy (==0.4.0.dev2)
19
+ Requires-Dist: tigrbl (==0.4.5.dev8)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # tba-durability-authorization-policy
23
+
24
+ Carrier-neutral layer-30 durable operations and derived table specifications.
25
+
@@ -0,0 +1,3 @@
1
+ # tba-durability-authorization-policy
2
+
3
+ Carrier-neutral layer-30 durable operations and derived table specifications.
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "tba-durability-authorization-policy"
3
+ version = "0.4.0.dev2"
4
+ description = "Carrier-neutral layer-30 durable table operations."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10,<3.15"
7
+ license = "Apache-2.0"
8
+ dependencies = [
9
+ "tba-contracts-authorization==0.4.0.dev3",
10
+ "tigrbl==0.4.5.dev8",
11
+ "tba-primitives-identity==0.4.0.dev2",
12
+ "tba-contracts-identity==0.4.0.dev2",
13
+ "tba-bases-replay==0.4.0.dev2",
14
+ "tba-contracts-security-trust==0.1.0",
15
+ "tba-storage-authorization-policy==0.4.0.dev2",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["poetry-core>=1.0.0"]
20
+ build-backend = "poetry.core.masonry.api"
21
+
22
+ [tool.poetry]
23
+ packages = [{ include = "tba_durability_authorization_policy", from = "src" }]
@@ -0,0 +1,12 @@
1
+ from .legacy import NormalizedAuthorization, NormalizedRoleGrant, normalize_legacy_authorization
2
+
3
+ try:
4
+ from .operations import *
5
+ from .specifications import *
6
+ from .inventory import *
7
+ except ImportError:
8
+ # Normalization is dependency-free so schema migration tooling can run before
9
+ # the optional Tigrbl storage runtime is installed.
10
+ pass
11
+
12
+ __all__ = [name for name in globals() if not name.startswith("_")]
@@ -0,0 +1,22 @@
1
+ """Immutable inventory of this durable family's table specifications."""
2
+
3
+ from . import specifications as _specifications
4
+
5
+ RUNTIME_TABLE_SPECS = tuple(
6
+ value
7
+ for name, value in vars(_specifications).items()
8
+ if name.endswith("RuntimeSpec") and getattr(value, "model", None) is not None
9
+ )
10
+ RUNTIME_TABLES = tuple(spec.model for spec in RUNTIME_TABLE_SPECS)
11
+ RUNTIME_OPERATION_BY_ALIAS = {
12
+ operation.alias: operation
13
+ for spec in RUNTIME_TABLE_SPECS
14
+ for operation in spec.ops
15
+ if operation.extra.get("owner_layer") == "30-storage-runtime"
16
+ }
17
+
18
+ __all__ = [
19
+ "RUNTIME_OPERATION_BY_ALIAS",
20
+ "RUNTIME_TABLE_SPECS",
21
+ "RUNTIME_TABLES",
22
+ ]
@@ -0,0 +1,45 @@
1
+ """Fail-closed normalization for the legacy role JSON representation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Iterable, Mapping
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class NormalizedRoleGrant:
11
+ grant_id: str
12
+ tenant_id: str
13
+ principal_id: str
14
+ role_id: str
15
+ resource_type: str
16
+ resource_id: str
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class NormalizedAuthorization:
21
+ role_permissions: tuple[tuple[str, str, str], ...]
22
+ role_grants: tuple[NormalizedRoleGrant, ...]
23
+
24
+
25
+ def normalize_legacy_authorization(*, roles: Iterable[Mapping[str, object]], memberships: Iterable[Mapping[str, object]]) -> NormalizedAuthorization:
26
+ by_key: dict[tuple[str, str], Mapping[str, object]] = {}
27
+ permissions: list[tuple[str, str, str]] = []
28
+ for role in roles:
29
+ tenant_id, name, role_id = str(role["tenant_id"]), str(role["name"]), str(role["id"])
30
+ key = (tenant_id, name)
31
+ if key in by_key:
32
+ raise ValueError(f"duplicate legacy role {tenant_id}/{name}")
33
+ by_key[key] = role
34
+ permissions.extend((role_id, str(permission), "allow") for permission in role.get("permissions", ()))
35
+ grants: list[NormalizedRoleGrant] = []
36
+ for membership in memberships:
37
+ tenant_id = str(membership["tenant_id"])
38
+ principal_id = str(membership["principal_id"])
39
+ for name in membership.get("roles", ()):
40
+ role = by_key.get((tenant_id, str(name)))
41
+ if role is None:
42
+ raise ValueError(f"unknown legacy role {tenant_id}/{name}")
43
+ role_id = str(role["id"])
44
+ grants.append(NormalizedRoleGrant(f"legacy:{tenant_id}:{principal_id}:{role_id}", tenant_id, principal_id, role_id, "tenant", tenant_id))
45
+ return NormalizedAuthorization(tuple(permissions), tuple(grants))
@@ -0,0 +1,231 @@
1
+ """Durable authorization membership and delegated-scope operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from tba_storage_authorization_policy.tables import AttributePolicy, DelegatedAdminScope, PolicyCondition, Role, TenantMembership
9
+
10
+ from tigrbl import (
11
+ create_table_record,
12
+ clear_table_records,
13
+ database_from_context,
14
+ field_value,
15
+ first_table_record,
16
+ list_table_records,
17
+ payload_from_context,
18
+ record_identifier,
19
+ update_table_record,
20
+ )
21
+
22
+
23
+ def _strings(values: Any) -> tuple[str, ...]:
24
+ if values is None or values == "" or values is False:
25
+ return ()
26
+ if isinstance(values, str):
27
+ return (values,)
28
+ return tuple(sorted({str(value) for value in values if value not in {None, ""}}))
29
+
30
+
31
+ async def grant_membership(ctx: Mapping[str, Any]) -> object:
32
+ values = dict(payload_from_context(ctx))
33
+ db = database_from_context(ctx)
34
+ tenant_id = str(values["tenant_id"])
35
+ principal_id = str(values["principal_id"])
36
+ payload = {
37
+ "tenant_id": tenant_id,
38
+ "principal_id": principal_id,
39
+ "roles": list(_strings(values.get("roles", ()))),
40
+ "status": str(values.get("status", "active")),
41
+ }
42
+ existing = await first_table_record(
43
+ TenantMembership,
44
+ db,
45
+ {"tenant_id": tenant_id, "principal_id": principal_id},
46
+ )
47
+ if existing is None:
48
+ return await create_table_record(TenantMembership, db, payload)
49
+ return await update_table_record(
50
+ TenantMembership, db, record_identifier(existing), payload
51
+ )
52
+
53
+
54
+ async def assign_role(ctx: Mapping[str, Any]) -> object:
55
+ values = dict(payload_from_context(ctx))
56
+ db = database_from_context(ctx)
57
+ filters = {
58
+ "tenant_id": values["tenant_id"],
59
+ "principal_id": values["principal_id"],
60
+ }
61
+ existing = await first_table_record(TenantMembership, db, filters)
62
+ roles = set(_strings(field_value(existing, "roles", ()))) if existing else set()
63
+ roles.add(str(values["role_name"]))
64
+ return await grant_membership(
65
+ {
66
+ "db": db,
67
+ "payload": {**filters, "roles": roles, "status": "active"},
68
+ }
69
+ )
70
+
71
+
72
+ async def role_names_for_principal(ctx: Mapping[str, Any]) -> tuple[str, ...]:
73
+ values = dict(payload_from_context(ctx))
74
+ filters = {"principal_id": values["principal_id"]}
75
+ if values.get("tenant_id") is not None:
76
+ filters["tenant_id"] = values["tenant_id"]
77
+ rows = await list_table_records(
78
+ TenantMembership, database_from_context(ctx), filters
79
+ )
80
+ roles: set[str] = set()
81
+ for row in rows:
82
+ if field_value(row, "status", "active") == "active":
83
+ roles.update(_strings(field_value(row, "roles", ())))
84
+ return tuple(sorted(roles))
85
+
86
+
87
+ async def upsert_role(ctx: Mapping[str, Any]) -> object:
88
+ values = dict(payload_from_context(ctx))
89
+ db = database_from_context(ctx)
90
+ filters = {
91
+ "name": str(values["name"]),
92
+ "tenant_id": values.get("tenant_id"),
93
+ }
94
+ payload = {
95
+ **filters,
96
+ "permissions": list(_strings(values.get("permissions", ()))),
97
+ "denied_permissions": list(_strings(values.get("denied_permissions", ()))),
98
+ "inherited_roles": list(_strings(values.get("inherited_roles", ()))),
99
+ "status": str(values.get("status", "active")),
100
+ }
101
+ existing = await first_table_record(Role, db, filters)
102
+ if existing is None:
103
+ return await create_table_record(Role, db, payload)
104
+ return await update_table_record(Role, db, record_identifier(existing), payload)
105
+
106
+
107
+ async def list_roles_for_tenant(ctx: Mapping[str, Any]) -> tuple[object, ...]:
108
+ values = dict(payload_from_context(ctx))
109
+ tenant_id = values.get("tenant_id")
110
+ rows = await list_table_records(Role, database_from_context(ctx))
111
+ return tuple(
112
+ row for row in rows if field_value(row, "tenant_id") in {None, tenant_id}
113
+ )
114
+
115
+
116
+ async def upsert_attribute_policy(
117
+ ctx: Mapping[str, Any],
118
+ ) -> tuple[object, list[object]]:
119
+ values = dict(payload_from_context(ctx))
120
+ db = database_from_context(ctx)
121
+ filters = {
122
+ "name": values["name"],
123
+ "tenant_id": values.get("tenant_id"),
124
+ }
125
+ existing = await first_table_record(AttributePolicy, db, filters)
126
+ payload = {
127
+ "name": values["name"],
128
+ "tenant_id": values.get("tenant_id"),
129
+ "client_id": values.get("client_id"),
130
+ "permission": values["permission"],
131
+ "effect": str(values.get("effect", "allow")),
132
+ "required_attributes": dict(values.get("required_attributes") or {}),
133
+ "status": "active",
134
+ }
135
+ if existing is None:
136
+ row = await create_table_record(AttributePolicy, db, payload)
137
+ else:
138
+ row = await update_table_record(
139
+ AttributePolicy, db, record_identifier(existing), payload
140
+ )
141
+ policy_id = record_identifier(row)
142
+ await clear_table_records(PolicyCondition, db, {"policy_id": policy_id})
143
+ conditions = [
144
+ await create_table_record(
145
+ PolicyCondition,
146
+ db,
147
+ {"policy_id": policy_id, **dict(condition)},
148
+ )
149
+ for condition in values.get("dynamic_conditions", ())
150
+ ]
151
+ return row, conditions
152
+
153
+
154
+ async def list_active_attribute_policies(
155
+ ctx: Mapping[str, Any],
156
+ ) -> tuple[tuple[object, list[object]], ...]:
157
+ values = dict(payload_from_context(ctx))
158
+ filters: dict[str, Any] = {"status": "active"}
159
+ if values.get("tenant_id") is not None:
160
+ filters["tenant_id"] = values["tenant_id"]
161
+ db = database_from_context(ctx)
162
+ rows = await list_table_records(AttributePolicy, db, filters)
163
+ collected = []
164
+ for row in rows:
165
+ conditions = await list_table_records(
166
+ PolicyCondition, db, {"policy_id": record_identifier(row)}
167
+ )
168
+ collected.append((row, conditions))
169
+ return tuple(collected)
170
+
171
+
172
+ async def grant_delegated_scope(ctx: Mapping[str, Any]) -> object:
173
+ values = dict(payload_from_context(ctx))
174
+ db = database_from_context(ctx)
175
+ subject = str(values.pop("subject"))
176
+ payload = {"subject": subject, "status": "active", **values}
177
+ existing = await first_table_record(DelegatedAdminScope, db, {"subject": subject})
178
+ if existing is None:
179
+ return await create_table_record(DelegatedAdminScope, db, payload)
180
+ return await update_table_record(
181
+ DelegatedAdminScope, db, record_identifier(existing), payload
182
+ )
183
+
184
+
185
+ async def revoke_delegated_scope(ctx: Mapping[str, Any]) -> object:
186
+ subject = str(payload_from_context(ctx).get("subject") or "")
187
+ db = database_from_context(ctx)
188
+ existing = await first_table_record(DelegatedAdminScope, db, {"subject": subject})
189
+ if existing is None:
190
+ raise LookupError(subject)
191
+ return await update_table_record(
192
+ DelegatedAdminScope,
193
+ db,
194
+ record_identifier(existing),
195
+ {"status": "revoked"},
196
+ )
197
+
198
+
199
+ async def delegated_scope_for_subject(ctx: Mapping[str, Any]) -> object | None:
200
+ subject = str(payload_from_context(ctx).get("subject") or "")
201
+ return await first_table_record(
202
+ DelegatedAdminScope,
203
+ database_from_context(ctx),
204
+ {"subject": subject},
205
+ )
206
+
207
+
208
+ async def list_active_delegated_scopes(
209
+ ctx: Mapping[str, Any],
210
+ ) -> tuple[object, ...]:
211
+ rows = await list_table_records(
212
+ DelegatedAdminScope,
213
+ database_from_context(ctx),
214
+ {"status": "active"},
215
+ )
216
+ return tuple(rows)
217
+
218
+
219
+ __all__ = [
220
+ "assign_role",
221
+ "delegated_scope_for_subject",
222
+ "grant_delegated_scope",
223
+ "grant_membership",
224
+ "list_active_attribute_policies",
225
+ "list_active_delegated_scopes",
226
+ "list_roles_for_tenant",
227
+ "revoke_delegated_scope",
228
+ "role_names_for_principal",
229
+ "upsert_role",
230
+ "upsert_attribute_policy",
231
+ ]
@@ -0,0 +1,172 @@
1
+ """Authorization table aliases and executable runtime specifications."""
2
+
3
+ from tba_storage_authorization_policy.tables import AttributePolicy, AuthorizationInvariant, DelegatedAdminScope, InvariantEvaluation, InvariantViolation, Policy, PolicyVersion, Role, TenantMembership
4
+
5
+ from tigrbl import deriveTableSpec
6
+ from tigrbl import makeOp
7
+ from tba_durability_authorization_policy.operations.authorization import (
8
+ assign_role,
9
+ delegated_scope_for_subject,
10
+ grant_delegated_scope,
11
+ grant_membership,
12
+ list_active_attribute_policies,
13
+ list_active_delegated_scopes,
14
+ list_roles_for_tenant,
15
+ revoke_delegated_scope,
16
+ role_names_for_principal,
17
+ upsert_role,
18
+ upsert_attribute_policy,
19
+ )
20
+
21
+ TenantMembershipTable = TenantMembership
22
+ DelegatedAdminScopeTable = DelegatedAdminScope
23
+ RoleTable = Role
24
+ AttributePolicyTable = AttributePolicy
25
+ AuthorizationInvariantTable = AuthorizationInvariant
26
+ InvariantEvaluationTable = InvariantEvaluation
27
+ InvariantViolationTable = InvariantViolation
28
+ PolicyTable = Policy
29
+ PolicyVersionTable = PolicyVersion
30
+
31
+ AttributePolicyRuntimeSpec = deriveTableSpec(
32
+ AttributePolicyTable,
33
+ ops=(
34
+ makeOp(
35
+ extra={"owner_layer": "30-storage-runtime"},
36
+ expose_routes=False,
37
+ expose_rpc=False,
38
+ expose_method=True,
39
+ tx_scope="read_write",
40
+ alias="upsert_with_conditions",
41
+ handler=upsert_attribute_policy,
42
+ ),
43
+ makeOp(
44
+ extra={"owner_layer": "30-storage-runtime"},
45
+ expose_routes=False,
46
+ expose_rpc=False,
47
+ expose_method=True,
48
+ alias="list_active_with_conditions",
49
+ handler=list_active_attribute_policies,
50
+ tx_scope="read_only",
51
+ persist="skip",
52
+ ),
53
+ ),
54
+ )
55
+
56
+ RoleRuntimeSpec = deriveTableSpec(
57
+ RoleTable,
58
+ ops=(
59
+ makeOp(
60
+ extra={"owner_layer": "30-storage-runtime"},
61
+ expose_routes=False,
62
+ expose_rpc=False,
63
+ expose_method=True,
64
+ tx_scope="read_write",
65
+ alias="create_role",
66
+ handler=upsert_role,
67
+ ),
68
+ makeOp(
69
+ extra={"owner_layer": "30-storage-runtime"},
70
+ expose_routes=False,
71
+ expose_rpc=False,
72
+ expose_method=True,
73
+ alias="list_for_tenant",
74
+ handler=list_roles_for_tenant,
75
+ tx_scope="read_only",
76
+ persist="skip",
77
+ ),
78
+ ),
79
+ )
80
+
81
+ TenantMembershipRuntimeSpec = deriveTableSpec(
82
+ TenantMembershipTable,
83
+ ops=(
84
+ makeOp(
85
+ extra={"owner_layer": "30-storage-runtime"},
86
+ expose_routes=False,
87
+ expose_rpc=False,
88
+ expose_method=True,
89
+ tx_scope="read_write",
90
+ alias="grant_membership",
91
+ handler=grant_membership,
92
+ ),
93
+ makeOp(
94
+ extra={"owner_layer": "30-storage-runtime"},
95
+ expose_routes=False,
96
+ expose_rpc=False,
97
+ expose_method=True,
98
+ tx_scope="read_write",
99
+ alias="assign_role",
100
+ handler=assign_role,
101
+ ),
102
+ makeOp(
103
+ extra={"owner_layer": "30-storage-runtime"},
104
+ expose_routes=False,
105
+ expose_rpc=False,
106
+ expose_method=True,
107
+ alias="role_names_for_principal",
108
+ handler=role_names_for_principal,
109
+ tx_scope="read_only",
110
+ persist="skip",
111
+ ),
112
+ ),
113
+ )
114
+ DelegatedAdminScopeRuntimeSpec = deriveTableSpec(
115
+ DelegatedAdminScopeTable,
116
+ ops=(
117
+ makeOp(
118
+ extra={"owner_layer": "30-storage-runtime"},
119
+ expose_routes=False,
120
+ expose_rpc=False,
121
+ expose_method=True,
122
+ tx_scope="read_write",
123
+ alias="grant_scope",
124
+ handler=grant_delegated_scope,
125
+ ),
126
+ makeOp(
127
+ extra={"owner_layer": "30-storage-runtime"},
128
+ expose_routes=False,
129
+ expose_rpc=False,
130
+ expose_method=True,
131
+ tx_scope="read_write",
132
+ alias="revoke_scope",
133
+ handler=revoke_delegated_scope,
134
+ ),
135
+ makeOp(
136
+ extra={"owner_layer": "30-storage-runtime"},
137
+ expose_routes=False,
138
+ expose_rpc=False,
139
+ expose_method=True,
140
+ alias="lookup",
141
+ handler=delegated_scope_for_subject,
142
+ tx_scope="read_only",
143
+ persist="skip",
144
+ ),
145
+ makeOp(
146
+ extra={"owner_layer": "30-storage-runtime"},
147
+ expose_routes=False,
148
+ expose_rpc=False,
149
+ expose_method=True,
150
+ alias="list_active",
151
+ handler=list_active_delegated_scopes,
152
+ tx_scope="read_only",
153
+ persist="skip",
154
+ ),
155
+ ),
156
+ )
157
+
158
+ __all__ = [
159
+ "AttributePolicyRuntimeSpec",
160
+ "AttributePolicyTable",
161
+ "AuthorizationInvariantTable",
162
+ "DelegatedAdminScopeRuntimeSpec",
163
+ "DelegatedAdminScopeTable",
164
+ "InvariantEvaluationTable",
165
+ "InvariantViolationTable",
166
+ "PolicyTable",
167
+ "PolicyVersionTable",
168
+ "TenantMembershipRuntimeSpec",
169
+ "TenantMembershipTable",
170
+ "RoleRuntimeSpec",
171
+ "RoleTable",
172
+ ]