Skip to content

security(membership): end a member's sessions only in the organization that acted (#831) - #846

Open
alex-dembele wants to merge 2 commits into
masterfrom
security/831-scope-membership-revocation
Open

alex-dembele wants to merge 2 commits into
masterfrom
security/831-scope-membership-revocation

Conversation

@alex-dembele

Copy link
Copy Markdown
Member

Closes #831

Problem

When an organization changed a member's role or withdrew their access, membership.Service called RevokeAllUserTokens(userID), which deleted every refresh token the person held. So a decision taken in A signed them out of B.

Change

  • internal/auth/token.go: new RevokeUserTokensInTenant(ctx, userID, tenantID), which deletes WHERE user_id = ? AND tenant_id = ?.
  • internal/application/membership: the SessionRevoker port now takes the tenant. SetStatus and ChangeRole revoke only in the organization that acted.
  • Password change and reset still call RevokeAllUserTokens (criterion 3). They are untouched.

Why no gap opens. Every refresh already re-checks the membership in the token's own organization and re-derives the role (RefreshTokenPair → orgResolver, wired at cmd/server/main.go:592 via SetOrgSessionResolver). A session in B is never renewed on the strength of A, and a session in A dies with its tokens.

Review note. The issue asked for a tech-lead review of the approach before implementation. The owner asked for it to be done directly. The approach is a scoping fix that reuses the existing tenant_id column. It does not change the session model. A review of this PR serves as that review.

Verification

$ go test ./internal/handler/ -run 'TestMembershipSessions_' -count=1 -v
    after deactivate in A: A refresh refused, B refresh ok (new token issued: true)
    after revoke in A: A refresh refused, B refresh ok (new token issued: true)
    after role change in A: A refresh refused, B refresh ok (new token issued: true)
--- PASS: TestMembershipSessions_WithdrawingAccessInAKeepsTheSessionInB
--- PASS: TestMembershipSessions_AccountLevelRevocationStillEndsEverySession

With the old behavior (revocation by user_id only), the test fails: the B session must survive a deactivate in A, refresh failed: invalid refresh token.

$ go test ./internal/handler/... ./internal/auth/... ./internal/application/... ./internal/middleware/... ./cmd/... -count=1
ok handler · ok handler/auth · ok auth · ok application/auth · ok application/membership · ok middleware · ok cmd/server (no FAIL)

Honest remainders

  • The test's org resolver is a stand-in that repeats the rule of resolveSessionForOrg (active membership required), because that function lives in package main. Production wiring is not exercised by this test.
  • An access token already issued in A stays valid until it expires (15 minutes). That is the existing session model, and it is unchanged.
  • SQLite only, no live Postgres pass. The query is a plain two-column DELETE.

…t acted (#831)

Changing a member's role or withdrawing their access in organization A
called RevokeAllUserTokens, which deleted every refresh token the person
held, so A's decision signed them out of organization B as well.

The membership service now revokes through RevokeUserTokensInTenant, which
deletes only the refresh tokens whose tenant_id is A. Sessions in other
organizations are unaffected, and are still re-checked against their own
membership on every refresh by the org session resolver. Password change
and reset keep revoking everything.

Signed-off-by: alex-dembele <alexandredembele16@gmail.com>
…e others alive (#831)

Signed-off-by: alex-dembele <alexandredembele16@gmail.com>

This branch has not been deployed

No deployments
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.

security(membership): withdrawing access in one organization revokes the person's sessions in every organization

1 participant