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_exists is 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

ACCESS_LEVELS

read-write, or read-only (GET).

MAX_LABEL

The longest label a share may carry (the grantee sees it as the space's name).

Classes

ShareStore(backend, *[, root, account_exists])

Share semantics over a MutableMapping.

Exceptions

ShareError(message, *[, code])

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 label a share may carry (the grantee sees it as the space’s name).

exception enlace_auth.auth.shares.ShareError(message, *, code='invalid')[source]#

Bases: ValueError

An invalid share. code says which: "no_account" or "invalid".

class enlace_auth.auth.shares.ShareStore(backend, *, root=None, account_exists=None)[source]#

Bases: object

Share semantics over a MutableMapping.

Parameters:
  • backend (MutableMapping) – the per-name store (e.g. factory("shares")).

  • root (Optional[Path]) – the filesystem directory backing backend, used only to list one app’s shares without scanning every key (as GrantStore does). 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. None skips the check (tests).

access(app_id, owner, grantee, *, now=None)[source]#

"rw", "ro" or None: what grantee may do with the data.

Return type:

Optional[str]

granted(app_id, owner, *, now=None)[source]#

Every share owner made in app_id, each with "active".

Return type:

list[dict]

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 index app_id, owner and grantee on what they get.

Return type:

list[dict]

received(app_id, grantee, *, now=None)[source]#

The active shares made to grantee in app_id.

Return type:

list[dict]

remove_account(email)[source]#

Delete every share email is part of, either side. Returns how many.

Anything stored under a key {app_id}/{owner}/{grantee} that names email goes, whatever the record holds (a damaged or null record included), and so does a complete record whose fields name email. Account deletion never raises on a damaged record.

Return type:

int

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.

Return type:

list[tuple[str, str]]

share(app_id, owner, grantee, *, access='rw', label=None, expires_at=None, granted_by=None, now=None)[source]#

Create or replace the share owner → grantee in app_id.

expires_at is epoch seconds UTC or None; turn a date string into it with enlace_auth.auth.grants.parse_expires_at() first.

Return type:

dict