Invalidate old invitations when organization membership changes
verify / verify (push) Successful in 4m15s

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
2026-09-05 00:12:40 -04:00
parent c0986168bc
commit f142ac23a9
4 changed files with 73 additions and 3 deletions
+49
View File
@@ -436,3 +436,52 @@ func TestRoleInvitationCreationIsAtomicAndRejectsUnknownRole(t *testing.T) {
})
}
}
func TestRoleInvitationCannotBypassMembershipChanges(t *testing.T) {
for _, optimistic := range []bool{false, true} {
t.Run(map[bool]string{false: "legacy removal", true: "optimistic removal"}[optimistic], func(t *testing.T) {
f := newRoleSetFixture(t)
oldToken, oldInvitation := f.invite(t, "buyer", "billing")
f.addMember(t)
if err := f.organizations.AcceptInvitation(t.Context(), oldToken, roleMember); !errors.Is(err, organizations.ErrInvitationNotFound) {
t.Fatalf("old invite elevated existing member=%v", err)
}
_, roles := f.bindings(t, roleMember)
if !slices.Equal(roles, []string{"member"}) {
t.Fatalf("roles changed=%v", roles)
}
remove := func() error {
if optimistic {
return f.organizations.RemoveMembershipIfCurrent(t.Context(), organizations.MembershipRemoval{OrganizationID: f.org.ID, UserID: roleMember, ActorUserID: roleOwner, ExpectedStatus: "active", RequestID: "request-remove"})
}
return f.organizations.RemoveMembership(t.Context(), f.org.ID, roleMember, roleOwner, "request-remove")
}
if _, err := f.store.db.Exec(`CREATE TRIGGER fail_removal BEFORE INSERT ON gwf_access_audit_events WHEN NEW.action='membership.remove' BEGIN SELECT RAISE(ABORT,'audit failure'); END`); err != nil {
t.Fatal(err)
}
if err := remove(); err == nil {
t.Fatal("unaudited removal succeeded")
}
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organization_memberships WHERE user_id=?`, roleMember, 1)
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organization_invitations WHERE id=? AND revoked_at IS NULL AND used_at IS NULL`, oldInvitation.ID, 1)
if _, err := f.store.db.Exec(`DROP TRIGGER fail_removal`); err != nil {
t.Fatal(err)
}
if err := remove(); err != nil {
t.Fatal(err)
}
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organization_invitations WHERE id=? AND revoked_at IS NOT NULL AND used_at IS NULL`, oldInvitation.ID, 1)
if err := f.organizations.AcceptInvitation(t.Context(), oldToken, roleMember); !errors.Is(err, organizations.ErrInvitationNotFound) {
t.Fatalf("old invite restored removed member=%v", err)
}
newToken, _ := f.invite(t, "buyer")
if err := f.organizations.AcceptInvitation(t.Context(), newToken, roleMember); err != nil {
t.Fatalf("intentional fresh invitation=%v", err)
}
_, roles = f.bindings(t, roleMember)
if !slices.Equal(roles, []string{"buyer"}) {
t.Fatalf("fresh roles=%v", roles)
}
})
}
}