Skip to content

Owner-granted data shares: let named users at one user's per-user data - #33

Merged
thorwhalen merged 4 commits into
mainfrom
data-shares
Oct 1, 2026
Merged

thorwhalen merged 4 commits into
mainfrom
data-shares

Conversation

@thorwhalen

@thorwhalen thorwhalen commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Owner-granted data shares. Grants say who may open an app; a share says who may act on someone else's data in it: in app A, the owner's per-user store is readable (and by default writable) by a named grantee, signed in as themselves. First consumer: a public maths-practice app where a child's sessions are shared with her parents.

  • ShareStore (auth/shares.py): (app, owner, grantee) records with access (rw/ro) and a grantee-facing label, beside grants under the platform store. A share names existing accounts only and is removed with either account (admin delete_user cascades).
  • Owner routes /auth/shares/{app} (list, grant, revoke; a grantee can leave), admin /_admin/api/shares, CLI share / list-shares / revoke-share.
  • Per-user store: ?owner= honoured through an active share (404 no_access otherwise; writes need rw), a list route GET /api/{app}/store?prefix= (walks only the owner's directory, sorted, capped), ETags and If-Match / If-None-Match: * conditional writes (412 with the current value), refusal of values that cannot round-trip (NaN, lone surrogates), and CSRF on writes via CSRFMiddleware(enforce_prefixes=...) although /api/ is otherwise exempt.
  • user_store = true in an app.toml (enlace >= 0.1.43) gives a public app a per-user store for its signed-in visitors.

Upgrading: store writes now need X-CSRF-Token; /api/{app}/store is reserved. No known deployment used either.

Design, its adversarial review, and a second adversarial review of this code (findings fixed in the second and third commits): misc/docs/decisions/0001-owner-granted-data-shares.md.

Tests: tests/test_shares.py (store, routes, router, an end-to-end public user_store app, CSRF, admin cascade, review fixes); full suite 437 passed.

Grants say who may open an app; a share says who may act on someone else's
data in it. ShareStore (auth/shares.py) keeps (app, owner, grantee) records
with an rw/ro access level and a grantee-facing label, beside grants under the
platform store. A share names existing accounts only and is removed with
either account (admin delete_user cascades).

- /auth/shares/{app}: the signed-in owner lists, grants and revokes; a
  grantee can leave. /_admin/api/shares and `enlace-auth share|list-shares|
  revoke-share` let an admin manage shares for an owner who cannot (a child).
- The per-user store honours ?owner= with an active share (404 otherwise,
  writes need rw), gains a list route (GET /api/{app}/store?prefix=, walking
  only the owner's directory, capped), ETags and If-Match / If-None-Match
  conditional writes (412 with the current value), and CSRF on writes
  (CSRFMiddleware.enforce_prefixes) although /api/ is otherwise exempt.
- An app gets a per-user store when it is protected:user or sets
  `user_store = true` (needs enlace>=0.1.42), so a public app can keep data
  for its signed-in visitors.

Design and its adversarial review: misc/docs/decisions/0001-owner-granted-data-shares.md.
…rom no_key, cap labels; correct the ADR (adversarial code review)
@thorwhalen
thorwhalen merged commit 05d1688 into main Oct 1, 2026
10 of 12 checks passed
@thorwhalen
thorwhalen deleted the data-shares branch October 1, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant