fix(invites): revoke pending user invites by ID - #367
Conversation
📝 Contributor License Agreement requiredBefore this PR can merge, every contributor must sign the Infisical CLA. Still needs to sign: Once everyone has signed, the check updates automatically — no need to close and reopen the PR. |
|
| Filename | Overview |
|---|---|
| internal/server/handle_users.go | Adds shared authorization and ID-based revoke/reinvite handlers, but checks authorization only after disclosing invitation existence and status. |
| internal/server/server.go | Replaces authenticated token-based administrative routes and store contracts with by-ID variants. |
| internal/store/sql_store.go | Adds invitation lookup and conditional revocation by database ID while removing obsolete token-based update methods. |
| web/src/pages/home/AllUsersTab.tsx | Maps listed invitations to IDs, revokes by ID, and renders the pending-invitation confirmation dialog. |
| cmd/user_invite.go | Displays invitation IDs and updates CLI revocation to call the authenticated by-ID endpoint. |
| internal/server/server_test.go | Updates the mock store and adds HTTP coverage for listing, revocation, invalid IDs, lifecycle state, and authorization. |
| internal/store/user_invite_test.go | Adds SQL-store coverage for ID lookup and pending-only revocation. |
Reviews (1): Last reviewed commit: "refactor(invites): share invite authoriz..." | Re-trigger Greptile
| inv, err := s.store.GetUserInviteByID(ctx, id) | ||
| if err != nil || inv == nil { | ||
| jsonError(w, http.StatusNotFound, "Invite not found") | ||
| return | ||
| } | ||
| if inv.Status != "pending" { | ||
| jsonError(w, http.StatusConflict, "Invite is not pending") | ||
| return | ||
| } |
There was a problem hiding this comment.
Invite state leaks before authorization
When an unrelated authenticated member submits predictable invitation IDs, this handler returns distinct 404 and 409 responses before calling canManageUserInvite, exposing which invitations exist and whether they remain pending; the reinvite handler has the same ordering. Move the authorization boundary ahead of these distinguishable state responses. How this was verified: Both handlers test ID existence and pending status before invoking the invitation-management authorization check.
|
I signed the CLA hours ago, but for some reason it isn't reflected in the PR. |
Summary
Fixes pending user-invite revocation.
The Users page listed pending invitations with a plaintext token field. The token is available only when an invitation is created. The database stores only its hash. As a result, listed invitations had an empty token and could not be revoked.
The page also did not render the confirmation dialog for pending invitations. Clicking Revoke invite closed the menu without sending a request.
This change:
GET /v1/users/invites;This follows the pending-agent invite pattern from #172.
Security follow-up
The ID is sequential. An unrelated authenticated member must not learn whether an invitation exists or whether it is pending. Revoke and reinvite now authorize the caller before checking invitation status. Unauthorized callers receive
404for both pending and non-pending invitations.Type of change
Test plan
make test)Passed:
go test ./internal/store ./internal/servermake buildgo test ./internal/serverafter the security follow-upAdded coverage for:
make testhas one unrelated environment-dependent CLI failure:TestResolveVaultWithProjectFile/falls_back_to_default_when_no_filereads an existing local default vault (user-default) instead of the test's expecteddefault. The store and server packages pass.Manual verification still needed:
/v1/users/invites/by-id/{id}.Security checklist