enlace_auth.auth.grants#
Runtime per-app access grants, with optional UTC expiry.
A grant gives a specific user (by email) access to a specific protected:user
app at runtime, without a redeploy. Grants are ADDITIVE on top of the static
allowed_users declared in an app’s app.toml: the effective allow-set for
an app is config allowed_users ∪ {active grants}. A grant is active when its
expires_at is None (never expires) or lies in the future (UTC).
Storage mirrors enlace_auth.auth.sessions.SessionStore: a thin adapter
over a MutableMapping whose keys are "{app_id}/{email}" and whose values
are JSON grant records:
{
"app_id": str,
"email": str, # normalized to lowercase
"expires_at": float | None, # epoch seconds, UTC; None = never
"granted_at": float, # epoch seconds, UTC
"granted_by": str | None, # admin email who created it
"note": str | None,
}
The composite key groups grants per app on disk (grants/{app_id}/{email}), so
the hot path — GrantStore.active_emails_for_app(), consulted by the auth
middleware on every protected:user request — lists a single app’s
subdirectory rather than scanning every grant for every app.
Functions
|
Coerce a user-supplied expiry into epoch seconds (UTC), or |
Classes
|
Thin adapter over a |
Exceptions
Raised when a grant argument is invalid (bad email/app_id, past expiry). |
- exception enlace_auth.auth.grants.GrantError[source]#
Bases:
ValueErrorRaised when a grant argument is invalid (bad email/app_id, past expiry).
- class enlace_auth.auth.grants.GrantStore(backend, *, root=None)[source]#
Bases:
objectThin adapter over a
MutableMappingthat speaks grant semantics.- Parameters:
backend (
MutableMapping) – the per-name store (e.g.factory("grants")).root (
Optional[Path]) – the resolved filesystem directory backingbackend(.../grants). Used ONLY for efficient per-app listing (root/{app_id}/*). All reads/writes go throughbackendso the storage codec stays in one place. WhenNone(e.g. a dict backend in tests), listing falls back to filtering the backend’s keys.
- active_emails_for_app(app_id, *, now=None)[source]#
The currently-active granted emails for
app_id(hot path).Each in both canonical spellings, so a grant written with a punycode domain admits the account keyed in Unicode, and vice versa.
- grant(app_id, email, *, expires_at=None, granted_by=None, note=None, now=None)[source]#
Create or replace a grant.
expires_atis epoch seconds UTC or None.Use
parse_expires_at()to turn a date/ISO string intoexpires_atbefore calling this.- Return type:
- enlace_auth.auth.grants.parse_expires_at(value, *, now=None)[source]#
Coerce a user-supplied expiry into epoch seconds (UTC), or
None.Accepts:
None/ empty string →None(never expires).a number → treated as epoch seconds, returned as a float.
"YYYY-MM-DD"→ end of that day UTC (23:59:59), so the grant is valid through the whole named day (least-surprising “expires on this date”).a full ISO-8601 timestamp (
...THH:MM[:SS][±TZ|Z]) → that instant. A trailingZis honored (Python 3.10’sfromisoformatcan’t) and a naive timestamp is assumed to be UTC.
Raises
GrantErrorfor unparseable input or an already-past expiry.