Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/services/auth/admin-mfa-gate.server.test.ts
T

151 lines
5.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { enforceAdminMfa } from "~/utils/auth/mfa/enforcement.server";
import { checkMfaStepUpState } from "~/utils/auth/mfa/step-up.server";
import { getUserSession } from "~/utils/session.server";
import { getUser, getUserTotpStatus } from "~/utils/db/users.db.server";
import { writeAdminSsoAuditEvent } from "~/modules/admin-sso/audit.server";
import {
requireAdminMfaForAction,
SENSITIVE_ADMIN_ACTIONS,
isSensitiveAdminAction,
} from "./admin-mfa-gate.server";
vi.mock("@toolctrl/config", () => ({
loadRequiredWebConfig: vi.fn(() => ({ routing: { appUrl: "https://app.example.test" } })),
}));
vi.mock("~/utils/auth/mfa/enforcement.server", () => ({
enforceAdminMfa: vi.fn(),
}));
vi.mock("~/utils/auth/mfa/step-up.server", () => ({
checkMfaStepUpState: vi.fn(),
}));
vi.mock("~/utils/app/CrossHostUrls", () => ({
absoluteAppRootPath: vi.fn(() => "https://app.example.test"),
}));
vi.mock("~/utils/session.server", () => ({
getUserSession: vi.fn(),
}));
vi.mock("~/utils/db/users.db.server", () => ({
getUser: vi.fn(),
getUserTotpStatus: vi.fn(),
}));
vi.mock("~/modules/admin-sso/audit.server", () => ({
writeAdminSsoAuditEvent: vi.fn(),
}));
const enforceAdminMfaMock = vi.mocked(enforceAdminMfa);
const checkMfaStepUpStateMock = vi.mocked(checkMfaStepUpState);
const getUserSessionMock = vi.mocked(getUserSession);
const getUserMock = vi.mocked(getUser);
const getUserTotpStatusMock = vi.mocked(getUserTotpStatus);
const writeAdminSsoAuditEventMock = vi.mocked(writeAdminSsoAuditEvent);
beforeEach(() => {
vi.clearAllMocks();
getUserSessionMock.mockResolvedValue({ get: vi.fn() } as never);
getUserMock.mockResolvedValue({ id: "user_1", admin: true } as never);
getUserTotpStatusMock.mockResolvedValue({ enabled: true } as never);
checkMfaStepUpStateMock.mockReturnValue({
verified: true,
state: { userId: "user_1", method: "totp", verifiedAt: "2026-05-25T12:00:00.000Z" },
ageMs: 1000,
});
enforceAdminMfaMock.mockReturnValue({ allowed: true, reason: "allowed", redirectTo: "/admin" });
writeAdminSsoAuditEventMock.mockResolvedValue(undefined);
});
describe("SENSITIVE_ADMIN_ACTIONS taxonomy", () => {
it("includes the spec-required actions", () => {
expect(SENSITIVE_ADMIN_ACTIONS).toContain("impersonation.start");
expect(SENSITIVE_ADMIN_ACTIONS).toContain("tenant.delete");
expect(SENSITIVE_ADMIN_ACTIONS).toContain("permission.grant");
expect(SENSITIVE_ADMIN_ACTIONS).toContain("audit.export");
expect(SENSITIVE_ADMIN_ACTIONS).toContain("data.export");
});
it("typeguard accepts known actions and rejects unknown", () => {
expect(isSensitiveAdminAction("impersonation.start")).toBe(true);
expect(isSensitiveAdminAction("tenant.delete")).toBe(true);
expect(isSensitiveAdminAction("totally-random")).toBe(false);
expect(isSensitiveAdminAction("")).toBe(false);
});
});
describe("requireAdminMfaForAction", () => {
it("returns the actor and writes success audit when MFA enforcement allows the action", async () => {
const request = new Request("https://admin.example.test/admin/accounts");
await expect(requireAdminMfaForAction(request, "tenant.delete", "user_1")).resolves.toEqual({
userId: "user_1",
action: "tenant.delete",
});
expect(getUserSessionMock).toHaveBeenCalledWith(request);
expect(getUserMock).toHaveBeenCalledWith("user_1");
expect(getUserTotpStatusMock).toHaveBeenCalledWith("user_1");
expect(enforceAdminMfaMock).toHaveBeenCalledWith(
expect.objectContaining({
requestUrl: request.url,
userId: "user_1",
isAdmin: true,
redirectMode: "host-aware",
appFallbackPath: "https://app.example.test",
}),
);
expect(writeAdminSsoAuditEventMock).toHaveBeenCalledWith(
expect.objectContaining({
action: "admin.mfa.app.challenge.success",
actorAdminUserId: "user_1",
request,
meta: { action: "tenant.delete" },
}),
);
});
it("audits and throws a redirect response when enforcement returns a Response", async () => {
const request = new Request("https://admin.example.test/admin/accounts");
const redirectResponse = new Response(null, {
status: 302,
headers: { Location: "/admin/security/mfa-challenge" },
});
enforceAdminMfaMock.mockReturnValue(redirectResponse as never);
await expect(requireAdminMfaForAction(request, "audit.export", "user_1")).rejects.toBe(redirectResponse);
expect(writeAdminSsoAuditEventMock).toHaveBeenCalledWith(
expect.objectContaining({
action: "admin.mfa.app.challenge.failure",
actorAdminUserId: "user_1",
request,
meta: { action: "audit.export", reason: "redirect-response" },
}),
);
});
it("audits denied enforcement and throws a redirect to the challenge path", async () => {
const request = new Request("https://admin.example.test/admin/accounts");
enforceAdminMfaMock.mockReturnValue({
allowed: false,
reason: "step-up-missing",
redirectTo: "/admin/security/mfa-challenge?redirectTo=%2Fadmin%2Faccounts",
});
await expect(requireAdminMfaForAction(request, "permission.grant", "user_1")).rejects.toMatchObject({
status: 302,
});
expect(writeAdminSsoAuditEventMock).toHaveBeenCalledWith(
expect.objectContaining({
action: "admin.mfa.app.challenge.failure",
actorAdminUserId: "user_1",
request,
meta: { action: "permission.grant", reason: "step-up-missing" },
}),
);
});
});