Replies: 4 comments
|
Progress update on the phased work tracked in #1658. Phases 1–3 have landed on the One deviation from the design above, in §3 ( No CIDR allowlist. A shared secret is required. Next.js does not expose the TCP peer address to route handlers, middleware or Server Actions ( Other decisions made during implementation, for reviewers of the final PR:
Next phases: host extension hooks, tenancy in |
|
Design update to §3 ("Built-in authenticators") before the integration branch lands on One identity entry point, composable auth methods, no built-in gateway authenticator (#1676):
The CIDR-vs-secret deviation described in my previous update no longer applies, since the gateway authenticator is gone. |
|
Update: the identity, tenancy and claims parts of this RFC landed on |
Uh oh!
There was an error while loading. Please reload this page.
RFC: Owner / identity seam, and server persistence as the only durable store
Status: Draft · Area: storage, auth · Supersedes nothing; builds on #1167, #1191, #1209, #1639, #944, #945, #982
Summary
OpenMAIC answers two questions separately and neither completely:
/api/persistence.anon:<uuid>cookie, a fixedPERSISTENCE_SHARED_OWNER_ID, or, for runtime data, a development bearer token plus a client-suppliedx-learner-key.The storage half is now solid (documents, runtime, assets with reclamation, per-owner quotas). The identity half is three independent resolvers, none of which is a production answer, and every deployment that needs real users has to fork the app to get one (#23, #582, Discussion #454, Discussion #404).
This RFC proposes:
OwnerAuthenticatorseam that every route, Server Action and background runner resolves identity through. Built-ins reproduce today's behavior, plus a trusted-proxy-header authenticator so real accounts (OIDC, SAML, LDAP via oauth2-proxy / Authelia / a gateway) work without code changes.DATABASE_URLis unset, the server falls back to an embedded PGlite database in a local data directory, sopnpm devand single-container Docker stay zero-config. Browser IndexedDB keeps only device preferences and caches. Deployments on serverless platforms (Vercel) or with more than one replica must setDATABASE_URL.Motivation
x-learner-key) and the open design questions in [Task] Asset conversion follow-ups: ownership alignment, conditional document writes, aggregate probe budgets #1129 and [Task] Learner runtime endpoints are unreachable in production builds without the insecure development authenticator #1413 all come from identity being chosen by the client instead of derived by the server.NEXT_PUBLIC_PERSISTENCE) must agree with runtime configuration (DATABASE_URL,PERSISTENCE_DEV_TOKEN), and the README spends a section on what happens when they disagree. Most new features (workbench, agent runtime, materials, skills) already require the server path.Non-goals
roles; deciding which roles exist beyond a small core vocabulary is host policy.trustedProxyHeader(§3) covers OIDC, SAML and LDAP through standard gateways. Revisit only if running a gateway proves to be a real adoption barrier.Design
1. The principal
Rules:
ownerId.startsWith('anon:')check becomesprincipal.kind === 'anonymous'or a role check (course:publish). Existinganon:<uuid>and un-prefixed shared ids remain valid.principalFromStoredOwner(ownerId)(§4).2. The seam
Contract:
setCookiesride every response, including 4xx/5xx, so minting is not lost on an error path./api/stages/*, folders, materials, agent sessions and skills, stage-meta, publish/unpublish and Server Actions all go through the seam. Today there are three resolvers (agent-runtime/owner.ts, the Server Action copy inworkspace-actions.ts, andpersistence/server-auth.ts); they are folded into it.mintAnonymous()that middleware may call on document requests.3. Built-in authenticators
anonymousCookie(default)anon:<uuid>httpOnly cookie.kind: 'anonymous', nocourse:publish.sharedTeamACCESS_CODEPERSISTENCE_SHARED_OWNER_ID, still requiresACCESS_CODE.kind: 'shared', may publish.trustedProxyHeaderX-Forwarded-User/X-Forwarded-Groups, only from requests whose peer is in a configured trusted-proxy CIDR list, or which carry a configured shared secret header. Anything else is a 401.kind: 'user',assurance: 'verified'.trustedProxyHeaderis the supported way to get real accounts: a gateway (oauth2-proxy, Authelia, Keycloak Gatekeeper, an institutional reverse proxy) performs the sign-in against the organization's identity provider and forwards the verified user in a header. OpenMAIC never handles passwords or tokens. The trust boundary is the pairing "only accept these headers from the gateway": deployments must restrict the app to the gateway's network, or configure the shared secret header, and the authenticator refuses to start without one of the two.Hosts with their own identity (platform JWTs, API keys, device bindings) implement
OwnerAuthenticatordirectly and register it at boot. That is the supported path instead of forking routes.4. Runtime data, assets and background work
principal.ownerIdin server mode.x-learner-keyandPERSISTENCE_DEV_TOKENare removed from the server path. This closes the GHSA-6r6j class and [Task] Learner runtime endpoints are unreachable in production builds without the insecure development authenticator #1413.ownerIdrecorded on its durable row, callsprincipalFromStoredOwner(ownerId)to getkindwithout re-authenticating, and callscanonicalizebefore creating rows, because a claim (§5) can land mid-run. Runners never build an id by string concatenation.5. Claiming anonymous work on sign-in
When a request carries a verified identity and a
pendingClaimfrom an anonymous cookie, the host may call:lockOwnerIdentity), with a fixed global participant order to avoid lock-order deadlocks.stage_meta, folders, owner materials, agent sessions and skills, runtime learner rows, asset ownership. Hosts register their own tables.owner_mergestable.canonicalizefollows it, and document creation under a merged-away id is refused.mergeLearnerbecomes a participant of the same transaction. The merge authorization in the storage handler becomes: allowed only forpendingClaim.fromOwnerId → principal.ownerIdwhere the target is not anonymous.6. Tenancy lives in
stage_metaonlystage_metaalready answers ownership, visibility and tombstones for every access decision (stage-access.ts).document_stages.owner_idduplicates it, and a second tenancy column doubles what a claim has to re-key. Proposal:PgDocumentStorebecomes tenant-agnostic; owner scoping is applied throughstage_meta. Migration: stop writingdocument_stages.owner_id, keep the column nullable for one release, backfillstage_metafrom it (the backfill already exists), then drop it.7. Host extension hooks
So hosts can add product behavior without forking the persistence route:
onCreate(tx, principal, stageId)/authorizeCreate(tx, principal, stageId)LibraryProvider.list(principal)GET /api/stageslists. Default: owned courses. A host may list saved or shared courses too.beforeAssetAllocate(principal, req)Responseto refuse.configureAssetByteStore(factory)ASSET_S3_BUCKETcheck so any byte store (S3, S3-compatible, other object stores) plugs in for both the route and the collector.8. Server persistence as the only durable store
DATABASE_URL→ embedded PGlite at./data/pglite(configurable), behind apg-compatible adapter that serializes transactions on PGlite's single session. A lock file prevents two processes from opening the same directory; the second process fails at boot.OPENMAIC_REQUIRE_DATABASE_URL=1makes a missingDATABASE_URLa boot error instead of a silent local database. Recommended for every hosted deployment.NEXT_PUBLIC_PERSISTENCEandNEXT_PUBLIC_PERSISTENCE_TOKENgo away; the client always uses the HTTP seams.GET /api/stagesmust work whenever persistence is available, not only whenOPENMAIC_AGENT_RUNTIME_ENABLEDis on.lib/document-store/migration.ts) is the base; media conversion and folder migration are added.Measured cost of the embedded default (spike, single machine): first open of a fresh data dir 0.6–0.8 s, boot to first persistence response 1.9 s, server RSS 167 MB → 470–560 MB, 27 MB on disk empty, +21.6 MB standalone output. All requests queue behind the single session, so the embedded default is for personal and small-team use.
Default-deployment exposure
With browser storage gone, a default deployment stores everyone's courses on the server under per-browser anonymous owners. Reads remain capability-by-id with unguessable server-issued ids (#1209, #1489); listing and writes are owner-scoped. A deployment reachable from the internet should set
ACCESS_CODEor an authenticator; the docs and the first-run banner say so.Phasing
OwnerAuthenticatorseam withanonymousCookie+sharedTeamreproducing today; fold the three resolvers; replaceanon:prefix checks withkind/ rolestrustedProxyHeader; runtime tombstone guardOPENMAIC_REQUIRE_DATABASE_URL, removeNEXT_PUBLIC_PERSISTENCE, library without agent runtime, browser importstage_metaonlydocument_stages.owner_id, then dropowner_merges, identity lockensure*ensure*Open questions
(owner_id, stage_id)so a library can hold saved courses too?pendingClaimis present, or always an explicit user action?References
All reactions