loom.core.authz¶
Role-based authorization primitives, free of any product vocabulary.
A product declares its permissions and roles in code, stores grants (who holds which role on which scope) wherever it likes, and asks the pure functions here for decisions. Scopes are paths whose meaning the product chooses; a grant on a scope reaches everything below it.
- class loom.core.authz.AuthzProduct(*args, **kwargs)[source]¶
Bases:
ProtocolWhat loom needs from a product to decide, delegate and elevate.
- class loom.core.authz.CompositionRules(custom_prefix, may_extend_base, non_extensible, ceiling)[source]¶
Bases:
objectWhat a product allows custom roles to do on top of its base catalog.
- Parameters:
- ceiling¶
The most a custom role may hold; the product passes the creator’s effective permissions at the boundary, never
None.
- class loom.core.authz.Decision(allowed, grant)[source]¶
Bases:
objectOutcome of an access check, truthy when allowed.
- grant¶
The grant that allowed it;
Nonewhen denied.- Type:
- class loom.core.authz.Grant(subject, role, scope, expires_at=None)[source]¶
Bases:
objectAn assignment of a role to a subject on a scope, possibly until a moment.
Grants order by subject, role, scope and then expiry, with grants that never expire before those that do, soonest first.
- subject¶
Opaque identifier of the holder, such as an identity subject or a service account id.
- Type:
- scope¶
Where the role applies, including everything below it.
- expires_at¶
The timezone-aware instant from which the grant allows nothing;
Nonewhen it never expires.- Type:
datetime.datetime | None
- class loom.core.authz.GrantCheck(allowed, missing)[source]¶
Bases:
objectOutcome of a delegation check, truthy when allowed.
- Parameters:
allowed (bool)
missing (frozenset[Permission])
- missing¶
Permissions the granter lacks on that scope.
- class loom.core.authz.GrantSource(*args, **kwargs)[source]¶
Bases:
ProtocolWhere the grants of a subject are loaded from.
Any class with this method satisfies the protocol structurally, with no inheritance required.
- class loom.core.authz.InMemoryGrantSource(grants)[source]¶
Bases:
objectA
GrantSourceover a fixed list, for tests and examples.- Parameters:
grants (Iterable[Grant])
- class loom.core.authz.Permission(name)[source]¶
Bases:
objectA capability the product protects, such as
"catalog.read".- Parameters:
name (str)
- class loom.core.authz.Role(name, permissions=())[source]¶
Bases:
objectA named set of permissions.
Grants store the role name only, so changing a role here changes what every stored grant of it allows.
- Parameters:
name (str)
permissions (frozenset[Permission])
- permissions¶
What the role allows.
- class loom.core.authz.RoleCatalog(permissions, roles)[source]¶
Bases:
objectEvery permission and role a product declares, validated once.
Example:
read, operate = Permission("catalog.read"), Permission("etl.operate") catalog = RoleCatalog( [read, operate], [Role("viewer", {read}), Role("operator", {read, operate})], )
- Parameters:
permissions (Iterable[Permission])
roles (Iterable[Role])
- classmethod from_specs(permissions, specs)[source]¶
Build a catalog from declarative specs, flattening
extends.- Parameters:
permissions (Iterable[Permission]) – Every permission the product declares.
specs (Mapping[str, RoleSpec]) – Role name to spec, in declaration order.
- Returns:
The catalog, equal to one built from the flattened roles.
- Raises:
ValueError – On a cycle, an unknown base or an undeclared permission.
- Return type:
- classmethod compose(base, custom, rules, *, namespace)[source]¶
Add custom roles to a base catalog under the product’s rules.
A custom role is named
f"{rules.custom_prefix}{namespace}/{name}", may only combine declared permissions, never exceeds the ceiling, never replaces a base role and never extends a non-extensible one.- Parameters:
base (RoleCatalog) – The fixed catalog the product ships.
rules (CompositionRules) – What custom roles may do.
namespace (str) – Where the custom roles belong, such as a boundary id.
- Returns:
A catalog with the base roles, unchanged, plus the custom ones.
- Raises:
ValueError – When the namespace, a name, an extension or a permission breaks the rules; the message names the offender.
- Return type:
- digest()[source]¶
Return a SHA-256 over the sorted
role:perm,permlines of the catalog.- Return type:
- property permissions: frozenset[Permission]¶
Every declared permission.
- role(name)[source]¶
Return the role declared as name.
- Parameters:
name (str) – Role name.
- Returns:
The declared role.
- Raises:
UnknownRole – When no role has that name.
- Return type:
- permissions_of(name)[source]¶
Return the permissions of role name, or
Nonewhen undeclared.- Parameters:
name (str) – Role name.
- Returns:
The role’s permissions, or
Nonefor a role the code does not declare.- Return type:
frozenset[Permission] | None
- roles_with(permission)[source]¶
Return the names of the roles that include permission.
- Parameters:
permission (Permission) – A declared permission.
- Returns:
The role names; empty when no role includes it.
- Raises:
UnknownPermission – When permission is not declared.
- Return type:
- require(permission)[source]¶
Reject a permission the product never declared.
- Parameters:
permission (Permission) – The permission about to be checked.
- Raises:
UnknownPermission – When permission is not declared.
- Return type:
None
- class loom.core.authz.RoleSpec(permissions=frozenset({}), extends=())[source]¶
Bases:
objectA role declared as data: its own permissions plus the roles it extends.
- class loom.core.authz.Scope(path)[source]¶
Bases:
objectA node in the product’s resource hierarchy.
loom gives the segments no meaning: the product decides what each level is. A scope covers itself and everything below it, so a grant on
Scope.of("acme")reachesScope.of("acme", "sales", "orders")but neverScope.of("globex")nor the root.str(scope)andparse()are inverse and stable, so the text form is safe to store.Example:
workspace = Scope.of("acme") table = workspace.child("sales", "orders") assert workspace.covers(table) assert Scope.parse(str(table)) == table
- classmethod root()[source]¶
Return the scope that covers every other scope.
- Returns:
The root scope.
- Return type:
- classmethod of(*segments)[source]¶
Build the scope at segments below the root.
- Parameters:
*segments (str) – One or more path segments, outermost first.
- Returns:
The scope at that path.
- Raises:
ValueError – When no segment is given (use
root()) or a segment is empty or contains/.- Return type:
- classmethod parse(text)[source]¶
Read a scope back from its
str()form.- Parameters:
text (str) –
/for the root, or/a/b.- Returns:
The scope text renders.
- Raises:
ValueError – When text does not start with
/, ends with one (other than the root) or holds an empty segment.- Return type:
- child(*segments)[source]¶
Return the scope at segments below this one.
- Parameters:
*segments (str) – Path segments to append, outermost first.
- Returns:
The nested scope.
- Raises:
ValueError – When a segment is empty or contains
/.- Return type:
- exception loom.core.authz.UnknownPermission[source]¶
Bases:
LookupErrorRaised when a permission is not declared in the catalog.
- exception loom.core.authz.UnknownRole[source]¶
Bases:
LookupErrorRaised when a role name is not declared in the catalog.
- loom.core.authz.can_grant(catalog, granter_grants, role, scope, *, delegate, now=None)[source]¶
Decide whether a granter may assign role on scope.
The granter needs delegate and every permission of role, each held on a scope covering scope by a grant that is alive at now.
- Parameters:
catalog (RoleCatalog) – The product’s roles.
role (str) – Name of the role to assign.
scope (Scope) – Where it would apply.
delegate (Permission) – The permission that allows assigning roles at all.
now (datetime | None) – The timezone-aware instant of the check; needed only when some of the granter’s grants carry an expiry.
- Returns:
The check, listing the permissions the granter lacks there.
- Raises:
UnknownRole – When role is not declared in the catalog.
UnknownPermission – When delegate is not declared in the catalog.
TypeError – When now is not a datetime.
ValueError – When now is naive, or is
Nonewhile some of the granter’s grants carry an expiry.
- Return type:
- loom.core.authz.can_revoke(catalog, granter_grants, grant, *, delegate, now=None)[source]¶
Decide whether a granter may remove grant.
Revoking needs what granting it would, whether or not grant has expired. A grant whose role the code does not declare allows nothing, so removing it needs delegate only.
- Parameters:
catalog (RoleCatalog) – The product’s roles.
grant (Grant) – The assignment to remove.
delegate (Permission) – The permission that allows assigning roles at all.
now (datetime | None) – The timezone-aware instant of the check; needed only when some of the granter’s grants carry an expiry.
- Returns:
The check, listing the permissions the granter lacks there.
- Raises:
UnknownPermission – When delegate is not declared in the catalog.
TypeError – When now is not a datetime.
ValueError – When now is naive, or is
Nonewhile some of the granter’s grants carry an expiry.
- Return type:
- loom.core.authz.evaluate(catalog, grants, permission, scope, *, now=None)[source]¶
Decide whether a subject holds permission on scope.
Denies unless one of grants names a role that includes permission on a scope covering scope. A grant whose role the catalog does not declare allows nothing. When several grants allow, the decision names the one on the deepest scope, then the first by role name and subject, regardless of the order of grants; among grants that tie there, the one that lives longest. A grant is alive while now is before its expiry; an expired grant counts as absent.
- Parameters:
catalog (RoleCatalog) – The product’s roles.
permission (Permission) – The capability being exercised.
scope (Scope) – Where it is exercised.
now (datetime | None) – The timezone-aware instant of the decision; needed only when some grant carries an expiry.
- Returns:
The decision, carrying the grant that allowed it.
- Raises:
UnknownPermission – When permission is not declared in the catalog.
TypeError – When now is not a datetime.
ValueError – When now is naive, or is
Nonewhile some grant carries an expiry.
- Return type:
- loom.core.authz.load_authz_product()[source]¶
Return the registered product, else the one the
loom.authzentry point names.- Return type:
AuthzProduct | None
- loom.core.authz.scopes_with(catalog, grants, permission, *, now=None)[source]¶
Return the outermost scopes on which a subject holds permission.
A scope covered by another returned scope is left out, so a listing can be filtered with one
Scope.covers()test per returned scope. An expired grant counts as absent.- Parameters:
catalog (RoleCatalog) – The product’s roles.
permission (Permission) – The capability being listed.
now (datetime | None) – The timezone-aware instant of the listing; needed only when some grant carries an expiry.
- Returns:
The minimal set of scopes covering everything the subject may reach.
- Raises:
UnknownPermission – When permission is not declared in the catalog.
TypeError – When now is not a datetime.
ValueError – When now is naive, or is
Nonewhile some grant carries an expiry.
- Return type: