145 lines
6.9 KiB
JavaScript
145 lines
6.9 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {mkdtempSync, readFileSync, rmSync} from "node:fs";
|
|
import {tmpdir} from "node:os";
|
|
import {join} from "node:path";
|
|
import {createAuthentikAdmin} from "./authentik.mjs";
|
|
|
|
const user = {
|
|
pk: 3,
|
|
uuid: "user-uuid",
|
|
username: "liooil",
|
|
name: "西腾",
|
|
email: "[email protected]",
|
|
is_active: true,
|
|
is_superuser: true,
|
|
type: "internal",
|
|
path: "users",
|
|
groups_obj: [{pk: "group-id", name: "liuhome"}],
|
|
last_login: null,
|
|
date_joined: "2026-01-01T00:00:00Z"
|
|
};
|
|
const group = {pk: "group-id", name: "liuhome", is_superuser: false, users: [3], users_obj: [{pk: 3, username: "liooil", name: "西腾"}]};
|
|
const application = {pk: "app-id", slug: "xiteng-chat", name: "Xiteng Chat"};
|
|
const binding = {pk: "binding-id", target: "app-id", group: "group-id", enabled: true};
|
|
|
|
function response(payload, status = 200) {
|
|
return new Response(payload === null ? null : JSON.stringify(payload), {status, headers: {"Content-Type": "application/json"}});
|
|
}
|
|
|
|
test("summarizes only human users and liuhome application access", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "authentik-admin-test-"));
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
const path = new URL(url).pathname;
|
|
if (path === "/-/health/ready/") return response({status: "ok"});
|
|
if (path === "/api/v3/core/users/") return response({results: [user, {...user, pk: 9, username: "ak-outpost", path: "goauthentik.io/outposts"}]});
|
|
if (path === "/api/v3/core/groups/") return response({results: [group]});
|
|
if (path === "/api/v3/core/applications/") return response({results: [application]});
|
|
if (path === "/api/v3/policies/bindings/") return response({results: [binding]});
|
|
if (path === "/api/v3/authenticators/admin/totp/") return response({results: [{pk: 1, name: "Phone", user: {pk: 3}}]});
|
|
if (path === "/api/v3/authenticators/admin/webauthn/") return response({results: [{pk: 2, name: "Laptop", user: {pk: 3}}]});
|
|
throw new Error(`Unexpected URL ${url}`);
|
|
};
|
|
try {
|
|
const admin = createAuthentikAdmin({baseUrl: "https://auth.example/", token: "token", auditPath: join(directory, "audit.jsonl")});
|
|
const summary = await admin.summary();
|
|
assert.deepEqual(summary.users.map((item) => item.username), ["liooil"]);
|
|
assert.deepEqual(summary.groups.map((item) => item.name), ["liuhome"]);
|
|
assert.deepEqual(summary.applications.find((item) => item.slug === "xiteng-chat").allowedGroups, ["liuhome"]);
|
|
assert.equal(summary.users[0].totpCount, 1);
|
|
assert.equal(summary.users[0].passkeyCount, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
rmSync(directory, {recursive: true, force: true});
|
|
}
|
|
});
|
|
|
|
test("refuses to disable liooil", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
if (new URL(url).pathname === "/api/v3/core/users/3/") return response(user);
|
|
throw new Error(`Unexpected URL ${url}`);
|
|
};
|
|
try {
|
|
const admin = createAuthentikAdmin({baseUrl: "https://auth.example/", token: "token", auditPath: "/tmp/unused-authentik-audit"});
|
|
await assert.rejects(() => admin.mutate("/api/admin/identity/users/3/disable", {}, "liooil"), (error) => {
|
|
assert.equal(error.statusCode, 409);
|
|
return /不能停用/.test(error.message);
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("does not write temporary passwords to the identity audit", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "authentik-audit-test-"));
|
|
const auditPath = join(directory, "audit.jsonl");
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, options = {}) => {
|
|
const path = new URL(url).pathname;
|
|
if (path === "/api/v3/core/users/3/") return response(user);
|
|
if (path === "/api/v3/core/users/3/set_password/" && options.method === "POST") return response(null, 204);
|
|
throw new Error(`Unexpected URL ${url}`);
|
|
};
|
|
try {
|
|
const admin = createAuthentikAdmin({baseUrl: "https://auth.example/", token: "token", auditPath});
|
|
await admin.mutate("/api/admin/identity/users/3/password", {password: "Temporary-secret-2026"}, "liooil");
|
|
const audit = readFileSync(auditPath, "utf8");
|
|
assert.doesNotMatch(audit, /Temporary-secret-2026/);
|
|
assert.match(audit, /user\.password/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
rmSync(directory, {recursive: true, force: true});
|
|
}
|
|
});
|
|
|
|
test("returns only the current user's authenticators and sessions", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
const parsed = new URL(url);
|
|
const path = parsed.pathname;
|
|
if (path === "/api/v3/core/users/") return response({results: [user]});
|
|
if (path === "/api/v3/authenticators/admin/totp/") return response({results: [
|
|
{pk: 1, name: "Phone", user: {pk: 3}},
|
|
{pk: 9, name: "Other", user: {pk: 99}}
|
|
]});
|
|
if (path === "/api/v3/authenticators/admin/webauthn/") return response({results: [
|
|
{pk: 2, name: "Laptop", created_on: "2026-01-02T00:00:00Z", device_type: "single_device", aaguid: "aaguid", user: {pk: 3}}
|
|
]});
|
|
if (path === "/api/v3/core/authenticated_sessions/") return response({results: [
|
|
{uuid: "session-id", user: 3, current: true, last_ip: "127.0.0.1", last_user_agent: "Browser", last_used: "2026-01-03T00:00:00Z", expires: "2026-01-04T00:00:00Z"},
|
|
{uuid: "other-session", user: 99}
|
|
]});
|
|
throw new Error(`Unexpected URL ${url}`);
|
|
};
|
|
try {
|
|
const admin = createAuthentikAdmin({baseUrl: "https://auth.example/", token: "token", auditPath: "/tmp/unused-authentik-audit"});
|
|
const account = await admin.accountSummary("liooil");
|
|
assert.equal(account.profile.username, "liooil");
|
|
assert.deepEqual(account.security.totp.map((device) => device.name), ["Phone"]);
|
|
assert.deepEqual(account.security.passkeys.map((device) => device.name), ["Laptop"]);
|
|
assert.deepEqual(account.security.sessions.map((session) => session.id), ["session-id"]);
|
|
assert.match(account.security.totpSetupUrl, /default-authenticator-totp-setup/);
|
|
assert.match(account.security.passkeySetupUrl, /default-authenticator-webauthn-setup/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("refuses to mutate another user's authenticator", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
const path = new URL(url).pathname;
|
|
if (path === "/api/v3/core/users/") return response({results: [user]});
|
|
if (path === "/api/v3/authenticators/admin/webauthn/9/") return response({pk: 9, name: "Other", user: {pk: 99}});
|
|
throw new Error(`Unexpected URL ${url}`);
|
|
};
|
|
try {
|
|
const admin = createAuthentikAdmin({baseUrl: "https://auth.example/", token: "token", auditPath: "/tmp/unused-authentik-audit"});
|
|
await assert.rejects(() => admin.accountMutate("/api/account/security/passkeys/9/delete", {}, "liooil"), (error) => error.statusCode === 404);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|