146 lines
9.8 KiB
Markdown
146 lines
9.8 KiB
Markdown
<!-- SPDX-License-Identifier: MPL-2.0 -->
|
|
|
|
# Web Foundations dogfood notes
|
|
|
|
This living note records concrete pressure discovered while Gamertan services
|
|
adopt Web Foundations. It is implementation evidence, not a promise that every
|
|
application concern belongs in the shared module.
|
|
|
|
## Gamertan accounts and commerce
|
|
|
|
- Personal identity editing is not instance administration. `OwnProfileRepository`
|
|
derives self-access from the active session; `ProfileEdit` is a trusted internal
|
|
command, never a browser request model. SQLite schema 11 adds a monotonic
|
|
revision because timestamps alone cannot distinguish two edits in one second.
|
|
Session/account/revision checks, mutation and audit share one write transaction.
|
|
Username edits invalidate other sessions without changing immutable IDs,
|
|
memberships, credentials, orders or provider billing identities. A password
|
|
proof binds the verified hash into that transaction; passkey proofs must bind
|
|
the exact user/session/field/value/revision before calling it. The application
|
|
chooses account-specific reauthentication and owns its credential-work limits.
|
|
Email requires a separate verified change protocol, not another accepted field.
|
|
|
|
- Instance operators need all-user/all-organization directories, not a staff
|
|
roster or implicit membership in every business. Optional bounded readers now
|
|
expose identity/profile records without credentials, independent of membership.
|
|
The application must authorize each call through an explicit instance scope;
|
|
these readers intentionally contain no Gamertan-specific roles or UI policy.
|
|
Stable-ID cursors and literal searches are covered against pagination gaps,
|
|
renamed profiles, inactive/personal records and wildcard/query injection.
|
|
- Customer profile and membership editing requires current ownership for every
|
|
write, not just changes involving another owner. The existing generic methods
|
|
intentionally permit application-authorized delegated administrators, so an
|
|
application preflight alone would leave a demotion race. Explicit owner-managed
|
|
methods now share their transactional cores while rechecking current direct
|
|
ownership before any write. Tests cover stale authority and optimistic state,
|
|
last-owner protection, concurrent winners, and audit-failure rollback. No extra
|
|
passkey ceremony or database migration is needed for this invariant.
|
|
- A customer may need both purchasing and billing access. Replacing one role at
|
|
a time would create partial permission states and misleading audit history.
|
|
The role-set extension commits all direct roles together with optimistic
|
|
binding IDs and current owner authority. Multiple-role invitations carry the
|
|
same combination atomically, with stored owner-managed policy rechecked when
|
|
accepted. SQLite tests cover concurrent winners, write rollback, demoted or
|
|
removed grantors, and attempted implicit reactivation of suspended members.
|
|
This adds schema 10; application vocabulary, allowed roles, invitation delivery,
|
|
ordinary-customer authentication, and UI/API commands remain application-owned.
|
|
- The public export allowlist omitted the owned-organization files introduced
|
|
in preview 22. Including them and building the exported tree tests the actual
|
|
distribution boundary rather than only comparing its path list with itself.
|
|
|
|
- Shared business purchasing exposed the difference between an initial member
|
|
and an initial RBAC owner. The historical organization creation method commits
|
|
membership but no access binding. The new `CreateOwnedOrganization` extension
|
|
grants the application-configured role and writes both audits atomically for
|
|
an existing active, fully registered user. It rejects missing roles and
|
|
unsupported adapters instead of leaving an ownerless organization behind.
|
|
SQLite tests inject failure at every write stage, including the second audit,
|
|
and race duplicate creates. Customer/merchant vocabulary remains application
|
|
policy; there is no new database schema or commerce dependency in Foundations.
|
|
|
|
- The account email remains required and unique. Gamertan uses normalized
|
|
email as the canonical login identifier; the immutable user ID, not the editable
|
|
username, owns account relationships. Until a mail package exists, it must not describe an
|
|
address as verified merely because it was entered during registration.
|
|
- Password authentication is sufficient for an ordinary customer base
|
|
session. Privileged application actions use an exact operation binding with
|
|
`authwebauthn.BeginApproval` and `FinishApproval`; that is safer than a broad
|
|
long-lived "elevated" session. A user without a passkey can use ordinary
|
|
features but must enroll one before performing protected work.
|
|
- `auth.Service.VerifyPassword` remains available for flows that truly require
|
|
password plus passkey before session issuance.
|
|
- Public registration exposed a cross-package transaction boundary. The
|
|
`account` package now keeps an unusable bounded registration draft and makes
|
|
recovery-code digests, personal organization, membership, owner binding,
|
|
activation, audits, and an optional initial passkey one repository commit.
|
|
A failed WebAuthn ceremony can be restarted, or an ordinary password account
|
|
can finish without it, without persisting a partly privileged account.
|
|
- Media belongs behind a storage-neutral interface with a hardened local
|
|
adapter. Content workflow, references, and authorization remain application
|
|
policy.
|
|
- Historical `authsqlite.Open` still migrates for compatibility. Applications
|
|
with reviewed deployment gates use `OpenWithOptions` with migration disabled,
|
|
require the current schema at startup, and invoke `Migrate` only from an
|
|
explicit operator command.
|
|
- Commerce remains a separately versioned nested module so payment-provider
|
|
policy and catalog evolution do not enlarge the authentication core.
|
|
- Self-service enrollment exposed an authorization seam: completing a valid
|
|
ceremony and checking its user only after persistence is too late.
|
|
`FinishRegistrationForUser` now consumes mismatched ceremonies and checks
|
|
the application-authenticated user before storing a credential.
|
|
- First-owner provisioning exposed another cross-package transaction boundary.
|
|
`bootstrap` now commits the passkey-only user, enrollment digest,
|
|
non-personal organization, membership, direct owner binding, and audits
|
|
together. Applications must seed their owner role first and must write the
|
|
returned raw token only to a newly created private file.
|
|
- Recovery-code consumption alone is not a complete recovery path. The
|
|
restricted grant must survive an interrupted authenticator prompt yet be
|
|
consumed in the same transaction that stores the verified replacement
|
|
passkey and replacement code digests. `authrecovery.BeginPasskey` and
|
|
`FinishPasskey` now provide that boundary without creating an authenticated
|
|
session; Gamertan keeps the raw grant only in a short-lived HttpOnly cookie.
|
|
- A portless-only WebAuthn origin rule made an unprivileged local HTTPS
|
|
exercise impossible even though WebAuthn origins include ports. The passkey
|
|
service now permits an explicit development port only when applications opt
|
|
in and the RP ID is `localhost` or reserved `.test`; production origins keep
|
|
the original portless default.
|
|
- Gamertan's staff-access page exposed a dangerous composition gap between
|
|
individual grant/revoke calls. Foundations now owns one optimistic,
|
|
transactional direct-role replacement that preserves the final active
|
|
owner and appends its audit before commit. The application still owns route
|
|
authorization, role presentation, CSRF, and the exact fresh-passkey
|
|
operation binding.
|
|
- Extending that page to membership suspension, reactivation, and removal
|
|
exposed the same time-of-check gap in the older lifecycle methods. The new
|
|
optimistic extension serializes on the active administrator membership,
|
|
rechecks the exact state bound into the passkey assertion, applies team and
|
|
direct-binding consequences, and writes the audit in one transaction.
|
|
- Human-assisted recovery cannot safely be expressed as a root command behind
|
|
an HTTP button. Preview 18 adds a distinct owner-assisted protocol: the
|
|
application performs the human review and fresh operation-bound passkey
|
|
ceremony, while the SQLite transaction rechecks an active direct owner,
|
|
invalidates every old account authenticator, stores only the grant digest,
|
|
and writes identity plus organization audits. Grant completion installs the
|
|
replacement password, passkey, and recovery-code set atomically and never
|
|
issues a session.
|
|
- Gamertan's distinction between Site Admin and Owner exposed a second
|
|
composition boundary: permission to manage ordinary staff must not imply
|
|
permission to create, demote, suspend, or remove an Owner. Preview 19 moves
|
|
that invariant into the same SQLite transactions as direct-role and
|
|
membership changes, while leaving the application's role vocabulary and UI
|
|
policy application-owned.
|
|
- Gamertan's invitation work found the same authority boundary before a route
|
|
was exposed: Site Admin must be able to invite ordinary staff without being
|
|
able to grant or cancel Owner access. Preview 20 passes the configured owner
|
|
role into invitation mutations and rechecks a current active direct Owner
|
|
after acquiring the SQLite write lock. The application still owns fresh
|
|
authentication, recipient delivery, and the one-time secret presentation.
|
|
- A real Bitwarden/Vaultwarden owner enrollment reached successful WebAuthn
|
|
verification but was rejected by a redundant algorithm check because the
|
|
application's direct response serializer omitted the optional browser
|
|
`publicKeyAlgorithm` convenience member. Preview 21 keeps ES256-only policy
|
|
enforcement but derives it from the verified COSE key embedded in
|
|
authenticator data. This makes the server independent of serializer-specific
|
|
convenience fields without weakening origin, challenge, user-verification,
|
|
or algorithm validation.
|