Commit fa677e9
committed
feat(consent): record consent in the same transaction as the user
The invariant this establishes: a user row without a consent record is
impossible. ResolveAll runs before the transaction opens, so an incomplete
payload never starts one; inside it, the user insert and the consent insert
both land or neither does.
user.Repository and the new user_consents repository each gain a Create that
takes a *sqlx.Tx. pkg/db has WithTxn but carries no transaction on the
context, so the transaction is threaded through explicitly rather than found
on one. Both are additive, and the user repository change is the one place
this feature reaches outside its own domain. The consent repository has
Create and nothing else, because the table is immutable.
consent.Grant writes one record for the documents it is given and has no
completeness rule of its own. ResolveAll is what decides a signup covers
every configured document, and keeping that out of Grant leaves room for a
later re-consent covering a subset without a second write path.
getOrCreateUser now has three outcomes for a new user. A complete payload
writes both rows in one transaction. An incomplete one returns
ErrConsentRequired and writes nothing. An existing user gets no record at
all, which is absolute: a record written outside a user creation would carry
that moment's timestamp and IP for an agreement made elsewhere, which is
worse than no record because it reads like evidence. A nil flow means one of
the paths that create a user without one, and those stay exempt because no
account holder is present to consent.
The completeness check runs at user creation under every intent, not for the
error but as the invariant guarding the write. An unset intent is permissive
for the login gate but never for consent. With app.consent disabled ResolveAll
resolves nothing, and an empty document set means write no record, so nothing
changes for a deployment that does not ask for consent.
Each signup also writes one audit record, with UserConsentGrantedEvent and
ConsentType added to pkg/auditrecord following the entity.verb naming already
there. It goes through the repository with the actor filled in, as userpat
does for its PAT events: the repository enriches an empty actor from the
context, and these endpoints are on the authentication skip list with no
actor in it, so the record would otherwise land as the system actor for an
act a person performed. It is written after the commit, since the audit
repository has no transactional create, so it cannot be atomic with the
record it describes. That is why the consent record is the source of truth
and this one is a breadcrumb: a failure is logged and the signup stands. Its
target metadata carries the whole document snapshot rather than the id and
the version alone, so a reader working from the audit trail can say what was
accepted without reading back a record they may not have access to.
The rollback is tested against a real Postgres rather than a mocked
transaction, since a mock can only pretend to roll back.
See docs/rfcs/0002-explicit-consent-at-signup.md, Enforcement and Storage.1 parent 2ef789b commit fa677e9
21 files changed
Lines changed: 1756 additions & 65 deletions
File tree
- cmd
- core
- authenticate
- mocks
- consent
- user
- mocks
- docs/rfcs
- internal/store/postgres
- pkg/auditrecord
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
349 | 349 | | |
350 | 350 | | |
351 | 351 | | |
352 | | - | |
353 | | - | |
354 | 352 | | |
355 | 353 | | |
356 | 354 | | |
| |||
399 | 397 | | |
400 | 398 | | |
401 | 399 | | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
402 | 404 | | |
403 | 405 | | |
404 | 406 | | |
| |||
450 | 452 | | |
451 | 453 | | |
452 | 454 | | |
453 | | - | |
| 455 | + | |
| 456 | + | |
454 | 457 | | |
455 | 458 | | |
456 | 459 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
0 commit comments