feat(security): isolate bounded work and support required auth actions
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { transformSync } = require("esbuild");
|
||||
const source = readFileSync(new URL("../src/App.tsx", import.meta.url), "utf8");
|
||||
const code = transformSync(source, { loader: "tsx", format: "cjs", target: "es2022" }).code;
|
||||
const context = vm.createContext({
|
||||
module: { exports: {} },
|
||||
require: (name) => name === "react" ? { lazy: () => null } : {}
|
||||
});
|
||||
context.exports = context.module.exports;
|
||||
vm.runInContext(code, context);
|
||||
const { normalizeAuthInfo, sessionMatchesAuth } = context.module.exports;
|
||||
const tenant = { id: "tenant-1", name: "Tenant", slug: "tenant" };
|
||||
const base = {
|
||||
user: { id: "membership-1", account_id: "account-1", email: "person@example.test", password_reset_required: true },
|
||||
tenant,
|
||||
principal: { auth_method: "session", account_id: "account-1", membership_id: "membership-1", session_id: "session-1", scopes: [] }
|
||||
};
|
||||
|
||||
test("normalization preserves an explicit required action and local password capability", () => {
|
||||
const normalized = normalizeAuthInfo({ ...base, user: { ...base.user, required_auth_action: "change_password", local_password: true } });
|
||||
assert.equal(normalized.user.required_auth_action, "change_password");
|
||||
assert.equal(normalized.user.local_password, true);
|
||||
assert.equal(normalized.scopes.length, 0);
|
||||
});
|
||||
|
||||
test("legacy password-reset metadata remains advisory without the server action", () => {
|
||||
const normalized = normalizeAuthInfo(base);
|
||||
assert.equal(normalized.user.password_reset_required, true);
|
||||
assert.equal(normalized.user.required_auth_action, null);
|
||||
assert.equal(normalized.user.local_password, false);
|
||||
});
|
||||
|
||||
test("lightweight session changes trigger full auth refresh for required actions and provider changes", () => {
|
||||
const auth = normalizeAuthInfo({ ...base, user: { ...base.user, required_auth_action: null, local_password: true } });
|
||||
const session = { user: { ...auth.user }, tenant, active_tenant: tenant, auth_method: "session", session_id: "session-1" };
|
||||
assert.equal(sessionMatchesAuth(session, auth), true);
|
||||
assert.equal(sessionMatchesAuth({ ...session, user: { ...session.user, required_auth_action: "change_password" } }, auth), false);
|
||||
assert.equal(sessionMatchesAuth({ ...session, user: { ...session.user, local_password: false } }, auth), false);
|
||||
assert.equal(sessionMatchesAuth({ ...session, session_id: "rotated-session" }, auth), false);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AuthInfo,
|
||||
AuthActionUiCapability,
|
||||
DashboardWidgetsUiCapability,
|
||||
OrganizationFunctionActionContext,
|
||||
OrganizationFunctionActionContribution,
|
||||
@@ -80,6 +81,19 @@ for (const testCase of cases) {
|
||||
assert(uiCapability("files.fileExplorer", [access, files]) === filesCapability, "files capability should return the module-provided object");
|
||||
assert(uiCapability("mail.profiles", [access, mail]) === mailCapability, "mail capability should return the module-provided object");
|
||||
|
||||
const authActionCapability: AuthActionUiCapability = {
|
||||
actions: ["change_password"], RequiredAction: () => null, LoginHelp: () => null
|
||||
};
|
||||
const publicAuthModule: PlatformWebModule = {
|
||||
id: "access", label: "Access", version: "test",
|
||||
publicRoutes: [{ path: "/password-recovery", render: () => null }],
|
||||
uiCapabilities: { "auth.actions": authActionCapability }
|
||||
};
|
||||
assert(uiCapability<AuthActionUiCapability>("auth.actions", [publicAuthModule]) === authActionCapability,
|
||||
"required authentication actions can resolve from the public catalogue without normal scopes");
|
||||
assert(uiCapability<AuthActionUiCapability>("auth.actions", []) === null,
|
||||
"core-only compositions have no authentication action implementation");
|
||||
|
||||
const configurableDashboardWidgets: DashboardWidgetsUiCapability = {
|
||||
widgets: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user