ποΈ PUT-1701: Add audit_team_membership and share.holder_group_id - #3708
Conversation
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||
4e4cb63 to
df5749a
Compare
df5749a to
12324e3
Compare
12324e3 to
31c17fa
Compare
31c17fa to
f0863f4
Compare
f0863f4 to
807416b
Compare
807416b to
5b041bc
Compare
5b041bc to
90cc78d
Compare
90cc78d to
f3c6fab
Compare
f3c6fab to
51bf3e7
Compare
Salazareo
left a comment
There was a problem hiding this comment.
looks fine, but I think we should expire and clean out audits after some amount of time
51bf3e7 to
b8cc83c
Compare
b8cc83c to
dfee763
Compare
dfee763 to
af16f0a
Compare
af16f0a to
e69b033
Compare
Insert-only record of what a workspace administrator did to an account, shaped like \`audit_user_to_group_permissions\` after 0019: nullable FK beside a NOT NULL \`_keep\` column. The FKs are ON DELETE SET NULL, never CASCADE, so hard-deleting an account cannot erase the record of the resets performed on it. Two indexes rather than one. The member's own view is the only place a reset becomes visible to the account it was performed on, so (user_id_keep, id) is a read path, not an optimisation. \`share.holder_group_id\` mirrors \`holder_user_id\` from 0067. The existing unique index does not constrain team shares at all -- it leads with \`holder_user_id\`, which is NULL on every team share, and NULLs are distinct -- so the group-scoped unique index is what prevents duplicates. Drops the \`role\` column from the specified DDL: it contradicted the settled single-administrator model.
e69b033 to
64db3fa
Compare
Local validation (post-merge)First execution of this migration's mysql DDL anywhere β the automated suite runs Boot 1 No errors in the boot log. Schema afterwards The three Boot 2 β idempotency The guarded The table is written by the real code path. After provisioning three seats and Actor attribution, target and timestamp all populate, ordered newest first. |
Last schema ticket of phase 1. Adds the one new table the feature needs, plus the
sharecolumn that lets a workspace-held share list like a user-held one. Ships dark β nothing reads either yet.audit_team_membershipShape follows
audit_user_to_group_permissionsas fixed in0019: a nullable FK beside aNOT NULL_keepcolumn, so a row still names its subject after the FK is blanked. (0016had the nullability inverted;0019is the one to copy.)Two properties are load-bearing:
ON DELETE SET NULL, neverCASCADE. With cascade, hard-deleting an account would erase the record of the password resets performed on it β the exact evidence the table exists to keep._keepcolumns areNOT NULL. They retain the identifiers after the FKs are blanked.Both are verified behaviourally below rather than by reading the DDL back.
Two indexes, not one
The ticket specified
idx_audit_team_membership_group. This also addsidx_audit_team_membership_useron(user_id_keep, id).That is not an optimisation. With a single administrator the audit log has no peer readership, so the member's own view is the only place a password reset becomes visible to the account it was performed on β design Β§5.5 leans on this directly. It is a first-class read path, and without the index it is a table scan. Adding it later would cost another three-dialect migration, so it goes in now.
The
rolecolumn is droppedThe ticket's DDL carried
"role" TEXT DEFAULT NULL. It is a leftover from the pre-reversal design and contradicts the settled model: PUT-1699 states there is no role column, and the design doc says roles do not exist in eight separate places. PUT-1699's own reasoning applies β "a nullable column nobody reads is an invitation to start reading it inconsistently".I removed it and reconciled the design doc, which had the same leftover in four other spots (Β§3 capability table, Β§10.5, the phase-2 scope list, and SR-4). The ticket DDL is updated to match.
share.holder_group_idMirrors
holder_user_idfrom0067/mysql_mig_22,CASCADEincluded β asharerow is a listing entry, not the grant itself, so it should go when its group does.β The existing unique index does not constrain team shares at all.
idx_share_holder_entry_issueris(holder_user_id, fsentry_id, issuer_user_id), and a team share leavesholder_user_idNULL. All three engines treat NULLs as distinct in a unique index, so every team share trivially satisfies it.idx_share_holder_group_entry_issueris what actually prevents a duplicate team share. Verified in both directions on sqlite and mysql below β distinct groups coexist, a genuine duplicate is refused.Verification
Postgres is verified live for the first time in this stack. I ran a
postgres:16container, which makesPostgresDatabaseClient.integration.test.tsβ this ticket's stated done-when β actually executable rather than deferred to CI.postgres 16 β verified against a live server
The first of those two applies the whole native chain to an empty database twice, which is the idempotency check the ticket asks for.
Confirming the run was real, not the pg-mock fallback
The test silently falls back to an in-memory mock when
PUTER_TEST_POSTGRES_URLis unset, and 2.7s looked fast for a 16-migration chain. Pointing it at a dead port must fail if the variable is honoured:It fails, so the passing run used the real server.
FK delete rules from the catalog
Chain applied into a scratch schema, then
pg_constraint.confdeltype(n= SET NULL,c= CASCADE):Matching the other two engines.
sqlite β verified
Three tests added:
applies the audit table and group-share columns from 0078β exact column list and all five indexes.keeps an audit row after its group is deleted, blanking only the FKβ inserts areset_member_passwordrow, deletes the group, assertsgroup_idis NULL whilegroup_id_keepandactionsurvive.constrains team shares that the user-holder index cannotβ two distinct-group shares of one file coexist; the duplicate is rejected.The second one is behavioural rather than a DDL string assertion because sqlite does enforce foreign keys here β
0043_add_dt.sqlends withPRAGMA foreign_keys = ONand never restores it, so the setting persists for the connection. Worth knowing; it is easy to assume otherwise since nothing inSqliteDatabaseClient.tssets it.mysql 8.4 β verified against a live server
Throwaway database, not the local dev
puterDB.Chain, idempotency, and table shape
FK rules and SET NULL behaviour
The row outlived its group, and still names which workspace and account it concerned.
The share indexes actually constrain
Both rows have
holder_user_idNULL, so the pre-existing unique index permitted them β the new one is doing the work.The throwaway database was dropped afterwards and the dev
puterDB left untouched (audit table in dev puter db: 0).typecheck
Review follow-up (
fix: index the audit foreign keys so deletes do not scan)The audit FK columns were unindexed on sqlite and postgres. All three are
ON DELETE SET NULL, so everyuserorgroupdelete has to find its child rows β and with no index that means scanning the whole append-only audit table, twice for a user sinceactor_user_idalso points atuser. Account hard-delete is a named operation in this feature, so the table gets scanned on exactly the path that matters, and it gets slower as the audit grows.mysql already had these, because InnoDB requires an index on a FK column β which is why the gap was invisible from the mysql side. Its
KEYs are renamed fromfk_*to the sameidx_*_fkscheme so all three dialects now read identically.Three-dialect index parity, dumped from live engines after the fix:
idx_audit_team_membership_groupgroup_id_keep, ididx_audit_team_membership_useruser_id_keep, ididx_audit_team_membership_{group,user,actor}_fkidx_share_holder_groupholder_group_id, ididx_share_holder_group_entry_issuerAlso fixed AND/OR precedence in the index-list assertion.
WHERE type = 'index' AND name LIKE ? OR name LIKE ?binds as(type='index' AND β¦) OR (β¦), leaving the second pattern unscoped to indexes β it passed by luck and would have matched a table or trigger with that prefix.Rebased onto current main.
Not fixed here β needs a decision
deleted_atplus a global unique index means the handle can never be reused, and mysql has no partial indexes, so this needs a deliberate answer (null the handle on delete, suffix it, or accept permanent reservation) rather than an index tweak. PUT-1702 territory.share.recipient_emailisNOT NULLin all three dialects, but a team share has no recipient email β the new test has to inventteam@test.localto insert a row, which is the smell. Either it becomes nullable or we commit to a documented sentinel.β Changed since approval
Approved at
51bf3e791. Renumbered only:0078_team-audit-and-group-shares.sql/
mysql_mig_32/postgres_mig_21. The table, its five indexes andshare.holder_group_idare unchanged.