mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
59772ffc34
* fix(members): a collection-restricted owner cannot grant beyond its scope through invitations or role changes (BUG-3634) A workspace owner restricted to specific collections could invite a second account (or any address). Accepting makes a member with collection_access 'all', who could read and write the hidden collections and then lift the first account's restriction. Promoting an unrestricted viewer gave write access over collections the owner could not see. - Invite: a restricted caller gets 403, since an invitation carries no scope (requireCallerCanSetCollectionAccess with "all"). - Role change: a restricted caller may change the role only of a member whose scope lies within its own, by the same helper. - Accept: AcceptWorkspaceInvitation re-reads the inviter's CURRENT scope in its transaction, so an invitation sent before this fix, or before the inviter was restricted, also grants nothing. It is refused, not narrowed: there is no scope to narrow to, and a silent downgrade would leave access nobody chose. All three doors answer inviter_scope_changed: the two accept routes with 403; register-with-code with 403 and the account rolled back when the code was the admission, or 201 with invitation_not_joined when registration is open. The invitation stays pending. The app-install door (bot membership with 'all') is the same class and is BUG-3768. Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * docs(members): why an admin inviter is exempt from the accept-time scope check (BUG-3634, codex r1) Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * fix(members): the accept share-locks the inviter's membership while it reads its scope (BUG-3634, codex r2) Round 2: requireInviterScopeTx read the inviter's collection_access without a lock, so a restriction committing alongside an accept could land after the read and before the membership. On Postgres the read is now FOR SHARE, and the restriction's UPDATE waits for the accept or is seen by it. A Postgres test holds a pending restriction and requires the accept to wait on it and then refuse. SQLite's single writer already orders the two. Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * fix(members): a restricted owner's role change re-checks the target's scope under the write's row lock (BUG-3634, codex r3) Round 3: the handler checked the target's scope and then wrote the role in a separate step, so an unrestricted owner widening the target in between let the role apply across collections the caller cannot see. UpdateWorkspaceMemberRoleWithin carries the checked set into the transaction. At the point where the UPDATE locks the row anyway (FOR UPDATE on Postgres, so the lock order is unchanged), it requires the target to be still restricted to a subset of that set, or refuses with ErrMemberScopeChanged (409 member_scope_changed). Unrestricted callers pass nil and check nothing. Each of the two guards was removed in turn, and the test went red each time. Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * docs(api): the role change's 409 member_scope_changed (BUG-3634, codex r4 nit) Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ