Hide delete account option when AUTH_TYPE is SSO#29
Conversation
|
@copilot Please review this PR. |
There was a problem hiding this comment.
Pull request overview
This PR aims to disable self-service account deletion when the instance is running in SSO (ForwardAuth) mode (AUTH_TYPE=SSO) by tightening authorization in the user policy and adding regression coverage for the API route and policy serialization.
Changes:
- Update
Userdeletion policy to deny self-delete whenenv.AUTH_TYPE === "SSO". - Add policy tests for delete behavior under SSO.
- Add API route tests ensuring self-delete is blocked under SSO while admin deletion of other users remains allowed.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| server/policies/user.ts | Adds AUTH_TYPE !== "SSO" gating to the self-delete ability check. |
| server/policies/user.test.ts | Adds tests validating delete ability behavior when AUTH_TYPE is set to SSO. |
| server/routes/api/users/users.test.ts | Adds API-level tests for /api/users.delete behavior under SSO. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| !!actor.team.getPreference(TeamPreference.MembersCanDeleteAccount), | ||
| env.AUTH_TYPE !== "SSO" | ||
| ) | ||
| ) |
| describe("when AUTH_TYPE is SSO", () => { | ||
| beforeEach(() => { | ||
| env.AUTH_TYPE = "SSO"; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| env.AUTH_TYPE = undefined; | ||
| }); | ||
|
|
||
| it("should not allow users to delete their own account", async () => { | ||
| const user = await buildUser(); | ||
| const abilities = serialize(user, user); | ||
| expect(abilities.delete).toBeFalsy(); | ||
| }); | ||
|
|
||
| it("should still allow admins to delete other users", async () => { | ||
| const admin = await buildAdmin(); | ||
| const user = await buildUser({ | ||
| teamId: admin.teamId, | ||
| }); | ||
| const abilities = serialize(admin, user); | ||
| expect(abilities.delete).toBeTruthy(); | ||
| }); | ||
| }); |
| describe("when AUTH_TYPE is SSO", () => { | ||
| beforeEach(() => { | ||
| env.AUTH_TYPE = "SSO"; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| env.AUTH_TYPE = undefined; | ||
| }); | ||
|
|
||
| it("should not allow users to delete their own account", async () => { | ||
| const user = await buildUser(); | ||
| await buildUser({ | ||
| teamId: user.teamId, | ||
| }); | ||
| const res = await server.post("/api/users.delete", { | ||
| body: { | ||
| code: user.deleteConfirmationCode, | ||
| token: user.getJwtToken(), | ||
| }, | ||
| }); | ||
| expect(res.status).toEqual(403); | ||
| }); | ||
|
|
||
| it("should still allow admins to delete other users", async () => { | ||
| const admin = await buildAdmin(); | ||
| const user = await buildUser({ | ||
| teamId: admin.teamId, | ||
| }); | ||
| const res = await server.post("/api/users.delete", { | ||
| body: { | ||
| id: user.id, | ||
| token: admin.getJwtToken(), | ||
| }, | ||
| }); | ||
| expect(res.status).toEqual(200); | ||
| }); |
Here's a review of the PR: Overall: The implementation is correct and well-structured. The policy change is minimal and surgical. ✅ What's goodPolicy change ( Frontend hiding is automatic Both endpoints are blocked Tests are correct
|
hunzlahmalik
left a comment
There was a problem hiding this comment.
Overview
Adds env.AUTH_TYPE !== "SSO" to the user delete ability in server/policies/user.ts. Since the policy is the single source of truth, the change cascades to:
- UI —
app/scenes/Settings/Preferences.tsx:300already gates the Danger → Delete account section oncan.delete. - API — both
users.requestDelete(server/routes/api/users/users.ts:621) andusers.delete(line 658) callauthorize(*, "delete", user).
Solid defense-in-depth from a surgical, one-line change.
Must-fix before merge
Admin self-delete is still permitted in SSO mode — see inline on server/policies/user.ts:73. The isTeamAdmin branch of the or(...) matches when actor === user (because isTeamAdmin(admin, admin) returns true), so the SSO guard is skipped for admins. If the intent is to block all self-service deletion under SSO, the fix is in the linked inline.
Should-fix
"Users can delete account" toggle stays visible in SSO mode — app/scenes/Settings/Security.tsx:309-321. Admins can still flip the MembersCanDeleteAccount preference but it is dead UI when AUTH_TYPE === "SSO". Either hide the row or disable the switch with a tooltip explaining the SSO override. (Out of diff, so noted here rather than inline.)
Cover users.requestDelete at the route layer — see inline on server/routes/api/users/users.test.ts.
Nits
afterEachresetsenv.AUTH_TYPEtoundefinedrather than capturing/restoring — see inline on the policy test file. Same pattern in the route test file.- Magic-string
"SSO"vs the existing localAUTH_TYPE_SSOconstant inserver/middlewares/authentication.ts:32-33— see inline onserver/policies/user.ts. The literal is also inlined inapp/stores/AuthStore.ts:217andapp/utils/ApiClient.ts:206,247; worth promoting to a shared constant alongside the@IsIn(["SSO"])declaration inserver/env.ts:553.
Looks good
- Single enforcement point (policy) correctly covers UI + both API routes.
- Tests at both policy and route layers with sensible naming.
- Env-mutation pattern matches existing precedent in
server/middlewares/authentication.test.ts. - Admin → delete-other-member path correctly preserved.
- No security regression; no perf impact.
Verdict
One blocker (admin self-delete gap) and two should-fixes worth addressing in the same PR. Once the blocker is resolved either way — and pinned with a test — this is good to merge.
| actor.id === user?.id, | ||
| !!actor.team.getPreference(TeamPreference.MembersCanDeleteAccount) | ||
| !!actor.team.getPreference(TeamPreference.MembersCanDeleteAccount), | ||
| env.AUTH_TYPE !== "SSO" |
There was a problem hiding this comment.
The admin self-delete path bypasses this guard. isTeamAdmin(admin, admin) returns true (admin is in their own team — see server/policies/utils.ts:76-81), so the or(isTeamAdmin(...), ...) short-circuits and the SSO check on this line never runs when an admin deletes themselves.
If the intent is to block all self-service delete under SSO (admin identities are also SSO-managed), an early-return at the top preserves the existing or structure cleanly:
allow(User, "delete", User, (actor, user) => {
if (env.AUTH_TYPE === "SSO" && actor.id === user?.id) {
return false;
}
return or(
isTeamAdmin(actor, user),
and(
actor.id === user?.id,
!!actor.team.getPreference(TeamPreference.MembersCanDeleteAccount)
)
);
});Whichever direction you take, pin the chosen behaviour with a test (admin self-delete in SSO returns delete: false, or returns true and is intentional).
Minor nit on the same line: "SSO" is already declared as const AUTH_TYPE_SSO = "SSO" in server/middlewares/authentication.ts:32-33 (local, unexported). The literal is also inlined in app/stores/AuthStore.ts:217 and app/utils/ApiClient.ts:206,247. Worth promoting to a shared constant so typos can't drift across the four call sites — server/env.ts:553 already constrains the value with @IsIn(["SSO"]).
| }); | ||
|
|
||
| afterEach(() => { | ||
| env.AUTH_TYPE = undefined; |
There was a problem hiding this comment.
env.AUTH_TYPE = undefined resets rather than restores. If the dev/CI env ever sets AUTH_TYPE, this silently shifts the baseline for later tests in the same file. Capture-and-restore is safer and matches the pattern in server/middlewares/rateLimiter.test.ts:13-20:
let originalAuthType: string | undefined;
beforeEach(() => {
originalAuthType = env.AUTH_TYPE;
env.AUTH_TYPE = "SSO";
});
afterEach(() => {
env.AUTH_TYPE = originalAuthType;
});Same applies to the equivalent block in server/routes/api/users/users.test.ts.
| }, | ||
| }); | ||
| expect(res.status).toEqual(200); | ||
| }); |
There was a problem hiding this comment.
Two gaps in this new block, both worth closing in the same PR:
-
Admin self-delete in SSO mode is unpinned. Once the policy gap (see inline on
server/policies/user.ts) is resolved, add a test where an admin callsPOST /api/users.deletewith noid(or with their ownid) and assert the expected status — 403 if you adopt the early-return fix. -
users.requestDeleteis listed in the PR description's test plan but isn't exercised here. Sameauthorize(user, "delete", user)call, so the policy unit test covers the mechanism, but one integration test locks the behaviour against future handler changes:
it("should not allow users to request their own deletion", async () => {
const user = await buildUser();
const res = await server.post("/api/users.requestDelete", {
body: { token: user.getJwtToken() },
});
expect(res.status).toEqual(403);
});|
@copilot Review again. |
All the review feedback has been addressed in commit Blockers fixed:
Should-fixes implemented:
Nits addressed:
Tests added:
CI is passing ✅ on commit |
Summary
AUTH_TYPEisSSO.can.delete.users.deleteandusers.requestDeletefor self-delete in SSO mode via existing authorization checks.Test plan
AUTH_TYPE=SSOon the server and restart.POST /api/users.deletewithout anid(self-delete): returns 403.users.deletewithid: still succeeds.AUTH_TYPEunset (or notSSO): Delete account still appears when Users can delete account is enabled.yarn test server/policies/user.test.tsyarn test server/routes/api/users/users.test.ts --testNamePattern="AUTH_TYPE is SSO"