Skip to content

fix: revoke access when a member is removed#14

Merged
TrueLineCollective merged 1 commit into
mainfrom
fix/revoke-access-on-member-removal
Jul 2, 2026
Merged

fix: revoke access when a member is removed#14
TrueLineCollective merged 1 commit into
mainfrom
fix/revoke-access-on-member-removal

Conversation

@TrueLineCollective

Copy link
Copy Markdown
Owner

The bug

resolveActor (the single chokepoint behind getActor / requireOwner) built an actor for any valid session or API key, and resolveRole defaulted to "member" when the user had no membership row. Two consequences, both confirmed against the code:

  • Removing a member did not revoke their access. Deleting the member row (what removeMember does) left the user's session and API keys working. They disappeared from the Team list, but could keep publishing, revoking, viewing stats, and minting keys.
  • Orphaned invites had access. An account that signed up through the invite gate but never completed acceptInvitation has a session and no membership, yet was treated as a "member".

The fix

Require a live workspace membership in resolveActor: no membership row means no actor, on both the session and the API-key path. This revokes access uniformly on removal (writes, stats, keys, forget, collections actions) and closes the orphaned-invite hole, for sessions and keys alike, through one place.

resolveRole is kept as a thin wrapper that still defaults to "member" for the dashboard pages that only use it to compute isAdmin (a non-member reading as a non-admin is harmless there). Authorization now flows through the new resolveMembership, which fails closed.

Scope note (a separate, smaller item for a follow-up)

After this change, a removed user can still load their own dashboard pages read-only until their session expires. They cannot mutate anything or see anyone else's data (every write/stats/keys call is now 401). Fully blocking the dashboard shell for non-members is a separate change because it also changes the login-page redirect behavior and needs a live smoke of the invite-acceptance flow, so I've left it out of this PR by design.

Verification

  • npm run typecheck clean
  • npm run lint clean
  • npm test 247/247 (added 5). New tests: resolveMembership (null for non-member, role for member), getActor returning null after the member row is deleted, a valid key with no membership rejected, and the stats endpoint going from 403 (authed member, not owner) to 401 (no longer authed) in production after removal.
  • npm run build exit 0

One fixture correction: setupWithSessions signed Bob up but never gave him a membership, relying on the old "member" default. It now inserts his member row (the accepted-invite state) so the IDOR tests still exercise a real member.

resolveActor built an actor for any valid session or API key and defaulted
the role to "member" whenever the user had no membership row. So removing a
member (deleting their member row) left their session and API keys fully
working, and an account that signed up but never accepted its invite was
treated as a member too.

Require a live workspace membership in resolveActor: no member row means no
actor, on both the session and the API-key path. Removal now revokes access
uniformly (all writes, stats, keys, forget, collections actions), and the
orphaned-invite hole is closed. resolveRole keeps its "member" default for
the dashboard pages that only use it to compute isAdmin, where a non-member
reading as a non-admin is harmless.

Tests: resolveMembership (null for non-member, role for member), getActor
returning null after the member row is deleted, and the stats endpoint going
from 403 to 401 in production once membership is removed.
@TrueLineCollective TrueLineCollective merged commit 9e6629c into main Jul 2, 2026
6 checks passed
@TrueLineCollective TrueLineCollective deleted the fix/revoke-access-on-member-removal branch July 2, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant