enlace_auth.auth.shares#
Owner-granted data shares: let named users at one user’s per-user data in one app.
Grants (enlace_auth.auth.grants) answer may this user open this app? A
share answers a different question: *may this user act on that user’s data in
this app?* A share is (app_id, owner, grantee) plus an access level, and the
per-user store (enlace_auth.stores.middleware) honours it when a request
names an owner with ?owner=. The design, and why it is shaped this way, is
misc/docs/decisions/0001-owner-granted-data-shares.md.
Storage mirrors GrantStore: a thin adapter over a
MutableMapping whose keys are "{app_id}/{owner}/{grantee}" and whose values
are JSON records:
{
"app_id": str,
"owner": str, # normalized email
"grantee": str, # normalized email
"access": "rw" | "ro", # "ro" = read-only (GET)
"label": str | None, # how the grantee sees this space
"granted_at": float, # epoch seconds, UTC
"granted_by": str | None, # who created it (the owner, or an admin)
"expires_at": float | None, # epoch seconds, UTC; None = never
}
Two rules the store enforces, so no caller can forget them:
A share names existing accounts only (when
account_existsis given). A share to an address nobody holds would be claimed by whoever registers it next.A share dies with either account:
ShareStore.remove_account()deletes every share an email is part of, and account deletion calls it.
Module Attributes
read-write, or read-only (GET). |
|
The longest |
Classes
|
Share semantics over a |
Exceptions
|
An invalid share. |
- enlace_auth.auth.shares.ACCESS_LEVELS = ('rw', 'ro')#
read-write, or read-only (GET).
- Type:
The access levels a share may carry
- enlace_auth.auth.shares.MAX_LABEL = 80#
The longest
labela share may carry (the grantee sees it as the space’s name).
- exception enlace_auth.auth.shares.ShareError(message, *, code='invalid')[source]#
Bases:
ValueErrorAn invalid share.
codesays which:"no_account"or"invalid".
- class enlace_auth.auth.shares.ShareStore(backend, *, root=None, account_exists=None)[source]#
Bases:
objectShare semantics over a
MutableMapping.- Parameters:
backend (
MutableMapping) – the per-name store (e.g.factory("shares")).root (
Optional[Path]) – the filesystem directory backingbackend, used only to list one app’s shares without scanning every key (asGrantStoredoes).None(a dict backend in tests) falls back to filtering all keys.account_exists (
Optional[Callable[[str],bool]]) –email -> bool. When given,share()refuses an owner or grantee with no account.Noneskips the check (tests).
- access(app_id, owner, grantee, *, now=None)[source]#
"rw","ro"orNone: whatgranteemay do with the data.
- list_all()[source]#
Every share in every app. Admin-only, infrequent.
Only genuine records are listed (see
_is_genuine()): a record that is damaged, or disagrees with its key, grants nothing, and callers can indexapp_id,ownerandgranteeon what they get.
- remove_account(email)[source]#
Delete every share
emailis part of, either side. Returns how many.Anything stored under a key
{app_id}/{owner}/{grantee}that namesemailgoes, whatever the record holds (a damaged or null record included), and so does a complete record whose fields nameemail. Account deletion never raises on a damaged record.- Return type:
- revoke(app_id, owner, grantee, *, widen_owner=False, widen_grantee=False, first_only=False)[source]#
Delete the share
owner->grantee; return the(owner, grantee)pairs removed (empty, so falsy, when there was none).A side that was typed (an admin’s pick, a URL segment) may be widened to both canonical spellings of its address, as
share()resolved it when the share was made. Never widen a side that is the caller’s own session key: its other spelling may be someone else’s account.With first_only, stop at the first share found, trying the spellings as written first: a user naming one owner or grantee means one account, though the two spellings may be two accounts. An admin revokes both.
- share(app_id, owner, grantee, *, access='rw', label=None, expires_at=None, granted_by=None, now=None)[source]#
Create or replace the share
owner→granteeinapp_id.expires_atis epoch seconds UTC orNone; turn a date string into it withenlace_auth.auth.grants.parse_expires_at()first.- Return type: