Added support for portal SSO with JWT authentication and local admin login. Updated environment configuration to include AUTH_REQUIRED, AUTH_JWT_SECRET, AUTH_ISSUER, and AUTH_PORTAL_URL. Enhanced the auth plugin to handle JWT verification based on the new configuration. Introduced new routes for authentication and updated the API client to manage token handling and redirects. Improved user experience by integrating authentication checks across various routes and components.
146 lines
4.0 KiB
TypeScript
146 lines
4.0 KiB
TypeScript
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
|
import { buildApp } from "../src/app.js";
|
|
import { loadConfig } from "../src/config.js";
|
|
import { hasPermission, permissionForRequest } from "../src/lib/permissions.js";
|
|
|
|
describe("permissions helpers", () => {
|
|
it("hasPermission respects admin ⊃ write ⊃ read", () => {
|
|
expect(hasPermission(["cfdm:domains:write"], "cfdm:domains:read")).toBe(
|
|
true,
|
|
);
|
|
expect(hasPermission(["cfdm:domains:admin"], "cfdm:domains:write")).toBe(
|
|
true,
|
|
);
|
|
expect(hasPermission(["cfdm:domains:read"], "cfdm:domains:write")).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("permissionForRequest maps domains and settings", () => {
|
|
expect(permissionForRequest("GET", "/api/v1/domains")).toBe(
|
|
"cfdm:domains:read",
|
|
);
|
|
expect(permissionForRequest("POST", "/api/v1/domains")).toBe(
|
|
"cfdm:domains:write",
|
|
);
|
|
expect(permissionForRequest("GET", "/api/v1/settings")).toBe(
|
|
"cfdm:settings:admin",
|
|
);
|
|
expect(permissionForRequest("POST", "/api/v1/sync/foo")).toBe(
|
|
"cfdm:domains:write",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("auth plugin (AUTH_REQUIRED)", () => {
|
|
const secret = "test-secret-at-least-8";
|
|
const issuer = "https://auth.shnt.top";
|
|
|
|
beforeAll(() => {
|
|
process.env.AUTH_REQUIRED = "true";
|
|
process.env.AUTH_JWT_SECRET = secret;
|
|
process.env.AUTH_ISSUER = issuer;
|
|
process.env.AUTH_PORTAL_URL = "http://localhost:5175";
|
|
});
|
|
|
|
afterAll(() => {
|
|
delete process.env.AUTH_REQUIRED;
|
|
delete process.env.AUTH_JWT_SECRET;
|
|
delete process.env.AUTH_ISSUER;
|
|
delete process.env.AUTH_PORTAL_URL;
|
|
});
|
|
|
|
it("GET /api/v1/auth/config exposes portal settings", async () => {
|
|
const app = await buildApp({
|
|
config: {
|
|
...loadConfig(),
|
|
authRequired: true,
|
|
jwtSecret: secret,
|
|
authIssuer: issuer,
|
|
authPortalUrl: "http://localhost:5175",
|
|
staticDir: null,
|
|
},
|
|
memory: true,
|
|
});
|
|
|
|
const res = await app.inject({ method: "GET", url: "/api/v1/auth/config" });
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.json()).toMatchObject({
|
|
required: true,
|
|
portal_url: "http://localhost:5175",
|
|
});
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("401 without token; 403 without cfdm app; 200 with rights", async () => {
|
|
const app = await buildApp({
|
|
config: {
|
|
...loadConfig(),
|
|
authRequired: true,
|
|
jwtSecret: secret,
|
|
authIssuer: issuer,
|
|
authPortalUrl: "http://localhost:5175",
|
|
staticDir: null,
|
|
},
|
|
memory: true,
|
|
});
|
|
|
|
const noAuth = await app.inject({ method: "GET", url: "/api/v1/domains" });
|
|
expect(noAuth.statusCode).toBe(401);
|
|
|
|
const tokenNoApp = app.jwt.sign(
|
|
{
|
|
sub: "u1",
|
|
email: "[email protected]",
|
|
name: "A",
|
|
apps: ["vps"],
|
|
permissions: ["cfdm:domains:read"],
|
|
iss: issuer,
|
|
},
|
|
{ expiresIn: "1h" },
|
|
);
|
|
const forbiddenApp = await app.inject({
|
|
method: "GET",
|
|
url: "/api/v1/domains",
|
|
headers: { authorization: `Bearer ${tokenNoApp}` },
|
|
});
|
|
expect(forbiddenApp.statusCode).toBe(403);
|
|
|
|
const okToken = app.jwt.sign(
|
|
{
|
|
sub: "u2",
|
|
email: "[email protected]",
|
|
name: "R",
|
|
apps: ["cfdm"],
|
|
permissions: ["cfdm:domains:read"],
|
|
iss: issuer,
|
|
},
|
|
{ expiresIn: "1h" },
|
|
);
|
|
const okRead = await app.inject({
|
|
method: "GET",
|
|
url: "/api/v1/domains",
|
|
headers: { authorization: `Bearer ${okToken}` },
|
|
});
|
|
expect(okRead.statusCode).toBe(200);
|
|
|
|
const denyWrite = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/domains",
|
|
headers: { authorization: `Bearer ${okToken}` },
|
|
payload: { name: "x" },
|
|
});
|
|
expect(denyWrite.statusCode).toBe(403);
|
|
|
|
const loginBlocked = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/auth/login",
|
|
payload: { username: "admin", password: "admin" },
|
|
});
|
|
expect(loginBlocked.statusCode).toBe(403);
|
|
|
|
await app.close();
|
|
});
|
|
});
|