Aari 09e25b8a32
fix(auth): let deployments close local self-registration (#4311)
* fix(auth): let deployments close local self-registration

The OIDC provisioning policy (allowed_email_domains, require_verified_email,
auto_create_users) is enforced only in the SSO callback via
get_or_provision_oidc_user. POST /api/v1/auth/register creates a local account
without consulting any of it, and nothing can turn that path off, so a
deployment declaring an email-domain allowlist can still be joined by any
address through local registration.

Add auth.local.allow_registration (default true, so existing deployments are
unchanged) and gate /register on it before the account is created. Report the
flag from /setup-status so the login page stops offering a signup entry the
Gateway will reject.

/initialize is deliberately not gated: it is the bootstrap path, guarded by
admin_count == 0, and closing it would leave a fresh install unable to create
its first admin.

An unreadable config.yaml falls back to the pre-gate default (open) rather than
making these two endpoints a hard dependency on the file.

* docs(auth): align registration-gate fallback wording with the FileNotFoundError catch
2026-07-20 23:33:09 +08:00

101 lines
2.6 KiB
TypeScript

import { afterEach, describe, expect, rs, test } from "@rstest/core";
import {
canCreateRegularAccount,
fetchSetupStatus,
isSystemAlreadyInitializedError,
setupStatusFetchInit,
} from "@/core/auth/setup";
describe("auth setup helpers", () => {
afterEach(() => {
rs.unstubAllGlobals();
});
test("setup-status requests bypass browser caches", () => {
expect(setupStatusFetchInit).toMatchObject({
cache: "no-store",
credentials: "include",
});
});
test("fetchSetupStatus uses the shared no-store request options", async () => {
const fetchMock = rs.fn(() =>
Promise.resolve(
new Response(JSON.stringify({ needs_setup: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
),
);
rs.stubGlobal("fetch", fetchMock);
await expect(fetchSetupStatus()).resolves.toEqual({ needs_setup: true });
expect(fetchMock).toHaveBeenCalledWith(
"/api/v1/auth/setup-status",
setupStatusFetchInit,
);
});
test("regular sign-up is disabled only while setup is required or unknown", () => {
expect(canCreateRegularAccount({ checked: false, status: null })).toBe(
false,
);
expect(
canCreateRegularAccount({
checked: true,
status: { needs_setup: true },
}),
).toBe(false);
expect(
canCreateRegularAccount({
checked: true,
status: { needs_setup: false },
}),
).toBe(true);
expect(canCreateRegularAccount({ checked: true, status: null })).toBe(true);
});
test("regular sign-up follows the gateway's registration_enabled flag", () => {
expect(
canCreateRegularAccount({
checked: true,
status: { needs_setup: false, registration_enabled: false },
}),
).toBe(false);
expect(
canCreateRegularAccount({
checked: true,
status: { needs_setup: false, registration_enabled: true },
}),
).toBe(true);
// Older Gateways omit the field; absent must not hide the signup entry.
expect(
canCreateRegularAccount({
checked: true,
status: { needs_setup: false },
}),
).toBe(true);
});
test("detects already-initialized setup conflicts", () => {
expect(
isSystemAlreadyInitializedError({
detail: {
code: "system_already_initialized",
message: "System already initialized",
},
}),
).toBe(true);
expect(
isSystemAlreadyInitializedError({
detail: {
code: "invalid_credentials",
message: "Wrong password",
},
}),
).toBe(false);
});
});