- Allowed guest accounts to be evaluated in `forward-auth`
- Validated `guest` account's `customScopes` and rejected ungranted access
- Added Array parameterization and `UNION` query in `getDashboardApps`
- Mapped `customScopes` to `getDashboardApps` in the UI route `/dashboard`
- Wired web and CLI joins in `events.ts` to `auditWrapper.auditLog` using correct schema (`event.id`, `{slug, method}`)
- Added `auditWrapper.auditLog` unit test validations in `events.test.ts`
- Added guest session scope unit tests in `forward_auth.test.ts`
- Moved Markdown tasks logic from `tasks/new/` to `tasks/complete/`
Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
374 lines
11 KiB
TypeScript
374 lines
11 KiB
TypeScript
import { assertEquals, assertExists } from "jsr:@std/assert";
|
|
import { stub } from "jsr:@std/testing/mock";
|
|
import { app } from "../main.ts";
|
|
import { sqlWrapper } from "../db.ts";
|
|
import { valkey } from "../valkey.ts";
|
|
import { rateLimitWrapper } from "../ratelimit.ts";
|
|
|
|
const originalSql = sqlWrapper.sql;
|
|
|
|
function setMockSql(mockImpl: () => Promise<any>) {
|
|
sqlWrapper.sql = mockImpl as any;
|
|
}
|
|
|
|
function restoreMockSql() {
|
|
sqlWrapper.sql = originalSql;
|
|
}
|
|
|
|
rateLimitWrapper.checkRateLimit = () => Promise.resolve(true);
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Valid session", async () => {
|
|
const mockUser = {
|
|
id: "user-id-1",
|
|
username: "alice",
|
|
account_status: "active",
|
|
};
|
|
|
|
setMockSql(() => Promise.resolve([mockUser]));
|
|
|
|
let queryCount = 0;
|
|
|
|
setMockSql(() => {
|
|
queryCount++;
|
|
if (queryCount === 1) return Promise.resolve([mockUser]); // User lookup
|
|
return Promise.resolve([]); // isGlobalAdmin lookup
|
|
});
|
|
|
|
const valkeyStub = stub(valkey, "get", (key: any) => {
|
|
const k = String(key);
|
|
if (k.startsWith("auth:app_by_host:")) {
|
|
return Promise.resolve(
|
|
JSON.stringify({ id: "app-id-1", name: "test-app" }),
|
|
);
|
|
}
|
|
if (k.startsWith("auth:grants:")) {
|
|
return Promise.resolve("viewer");
|
|
}
|
|
return Promise.resolve(
|
|
JSON.stringify({ uuid: "user-id-1", username: "alice" }),
|
|
);
|
|
});
|
|
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
Cookie: "session_id=mock-session-id",
|
|
"X-Forwarded-Host": "test.app.local",
|
|
},
|
|
});
|
|
|
|
const res = await app.request(req);
|
|
assertEquals(res.status, 200);
|
|
assertEquals(res.headers.get("X-Forwarded-User"), "alice");
|
|
assertEquals(res.headers.get("X-Forwarded-User-Id"), "user-id-1");
|
|
assertEquals(res.headers.get("X-Forwarded-Scopes"), "viewer");
|
|
assertEquals(res.headers.get("X-Forwarded-App-Id"), "app-id-1");
|
|
|
|
restoreMockSql();
|
|
valkeyStub.restore();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Missing session (API request)", async () => {
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: { "X-Forwarded-Host": "test.app.local" },
|
|
});
|
|
|
|
const valkeyStub = stub(valkey, "get", (key: any) => {
|
|
const k = String(key);
|
|
if (k.startsWith("auth:app_by_host:")) {
|
|
return Promise.resolve(
|
|
JSON.stringify({ id: "app-id-1", name: "test-app" }),
|
|
);
|
|
}
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
const res = await app.request(req);
|
|
assertEquals(res.status, 401);
|
|
valkeyStub.restore();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Missing session (Browser request with text/html)", async () => {
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
"X-Forwarded-Host": "ed-droid.atyg.org",
|
|
"X-Forwarded-Uri": "/control-panel",
|
|
"X-Forwarded-Proto": "https",
|
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
},
|
|
});
|
|
|
|
const valkeyStub = stub(valkey, "get", (key: any) => {
|
|
const k = String(key);
|
|
if (k.startsWith("auth:app_by_host:")) {
|
|
return Promise.resolve(
|
|
JSON.stringify({ id: "app-id-1", name: "ed-droid" }),
|
|
);
|
|
}
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
const res = await app.request(req);
|
|
assertEquals(res.status, 302);
|
|
const location = res.headers.get("Location");
|
|
assertExists(location);
|
|
assertEquals(
|
|
location?.includes(
|
|
"login?redirect=https%3A%2F%2Fed-droid.atyg.org%2Fcontrol-panel",
|
|
),
|
|
true,
|
|
);
|
|
valkeyStub.restore();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Expired session", async () => {
|
|
const valkeyStub = stub(valkey, "get", (key: any) => {
|
|
const k = String(key);
|
|
if (k.startsWith("auth:app_by_host:")) {
|
|
return Promise.resolve(
|
|
JSON.stringify({ id: "app-id-1", name: "test-app" }),
|
|
);
|
|
}
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
Cookie: "session_id=expired-session-id",
|
|
"X-Forwarded-Host": "test.app.local",
|
|
},
|
|
});
|
|
const res = await app.request(req);
|
|
assertEquals(res.status, 401);
|
|
valkeyStub.restore();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Suspended account", async () => {
|
|
const mockUser = {
|
|
id: "user-id-1",
|
|
username: "alice",
|
|
account_status: "suspended",
|
|
};
|
|
|
|
setMockSql(() => Promise.resolve([mockUser]));
|
|
|
|
const valkeyStub = stub(valkey, "get", (key: any) => {
|
|
const k = String(key);
|
|
if (k.startsWith("auth:app_by_host:")) {
|
|
return Promise.resolve(
|
|
JSON.stringify({ id: "app-id-1", name: "test-app" }),
|
|
);
|
|
}
|
|
return Promise.resolve(
|
|
JSON.stringify({ uuid: "user-id-1", username: "alice" }),
|
|
);
|
|
});
|
|
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
Cookie: "session_id=mock-session-id",
|
|
"X-Forwarded-Host": "test.app.local",
|
|
},
|
|
});
|
|
const res = await app.request(req);
|
|
assertEquals(res.status, 403);
|
|
assertEquals(await res.text(), "Forbidden: Account inactive");
|
|
|
|
restoreMockSql();
|
|
valkeyStub.restore();
|
|
});
|
|
|
|
Deno.test("Cookie Domain Scoping - getCookieDomain derives wildcard parent domain", async () => {
|
|
const { getCookieDomain } = await import("../auth-session.ts");
|
|
|
|
assertEquals(getCookieDomain("auth.atyg.org"), ".atyg.org");
|
|
assertEquals(getCookieDomain("ed-droid.atyg.org"), ".atyg.org");
|
|
assertEquals(getCookieDomain("atyg.org"), ".atyg.org");
|
|
assertEquals(getCookieDomain("localhost"), undefined);
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Unregistered domain (API)", async () => {
|
|
// Override sql to return no app
|
|
sqlWrapper.sql = (async () => []) as any;
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
"X-Forwarded-Host": "unknown.atyg.org",
|
|
},
|
|
});
|
|
const res = await app.fetch(req);
|
|
assertEquals(res.status, 403);
|
|
const data = await res.json();
|
|
assertEquals(data.error, "Application not registered");
|
|
restoreMockSql();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Unregistered domain (Browser)", async () => {
|
|
// Override sql to return no app
|
|
sqlWrapper.sql = (async () => []) as any;
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
"X-Forwarded-Host": "unknown.atyg.org",
|
|
"Accept": "text/html",
|
|
},
|
|
});
|
|
const res = await app.fetch(req);
|
|
assertEquals(res.status, 302);
|
|
const location = res.headers.get("Location") || "";
|
|
assertEquals(
|
|
true,
|
|
location.includes("/errors/unregistered?host=unknown.atyg.org"),
|
|
);
|
|
restoreMockSql();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Dynamic Bypass (is_public)", async () => {
|
|
// Override sql to return public app
|
|
sqlWrapper.sql = (async (strings: any) => {
|
|
if (strings[0].includes("FROM apps")) {
|
|
return [{
|
|
id: "public-app-id",
|
|
name: "Public App",
|
|
is_public: true,
|
|
}];
|
|
}
|
|
return [];
|
|
}) as any;
|
|
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
"X-Forwarded-Host": "public.atyg.org",
|
|
},
|
|
});
|
|
const res = await app.fetch(req);
|
|
assertEquals(res.status, 200);
|
|
assertEquals(res.headers.get("X-Forwarded-App-Id"), "public-app-id");
|
|
restoreMockSql();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Dynamic Bypass (bypass_paths)", async () => {
|
|
// Override sql to return app with bypass path
|
|
sqlWrapper.sql = (async (strings: any) => {
|
|
if (strings[0].includes("FROM apps")) {
|
|
return [{
|
|
id: "bypass-app-id",
|
|
name: "Bypass App",
|
|
bypass_paths: ["/api/public/*"],
|
|
}];
|
|
}
|
|
return [];
|
|
}) as any;
|
|
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
"X-Forwarded-Host": "bypass.atyg.org",
|
|
"X-Forwarded-Uri": "/api/public/status",
|
|
},
|
|
});
|
|
const res = await app.fetch(req);
|
|
assertEquals(res.status, 200);
|
|
restoreMockSql();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Guest session with app scope allowed", async () => {
|
|
const mockUser = {
|
|
id: "guest-id-1",
|
|
username: "guest_event_1",
|
|
account_status: "guest",
|
|
};
|
|
|
|
setMockSql(() => Promise.resolve([mockUser]));
|
|
|
|
const valkeyStub = stub(valkey, "get", (key: any) => {
|
|
const k = String(key);
|
|
if (k.startsWith("auth:app_by_host:")) {
|
|
return Promise.resolve(
|
|
JSON.stringify({ id: "app-id-1", name: "test-app" }),
|
|
);
|
|
}
|
|
return Promise.resolve(
|
|
JSON.stringify({
|
|
uuid: "guest-id-1",
|
|
username: "guest_event_1",
|
|
account_status: "guest",
|
|
customScopes: ["app:test-app", "viewer"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
Cookie: "session_id=mock-guest-session",
|
|
"X-Forwarded-Host": "test.app.local",
|
|
},
|
|
});
|
|
|
|
const res = await app.request(req);
|
|
assertEquals(res.status, 200);
|
|
assertEquals(res.headers.get("X-Forwarded-User"), "guest_event_1");
|
|
assertEquals(res.headers.get("X-Forwarded-User-Id"), "guest-id-1");
|
|
assertEquals(res.headers.get("X-Forwarded-Scopes"), "app:test-app,viewer");
|
|
assertEquals(res.headers.get("X-Forwarded-App-Id"), "app-id-1");
|
|
|
|
restoreMockSql();
|
|
valkeyStub.restore();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: GET /api/forward-auth - Guest session without app scope rejected (403)", async () => {
|
|
const mockUser = {
|
|
id: "guest-id-1",
|
|
username: "guest_event_1",
|
|
account_status: "guest",
|
|
};
|
|
|
|
setMockSql(() => Promise.resolve([mockUser]));
|
|
|
|
const valkeyStub = stub(valkey, "get", (key: any) => {
|
|
const k = String(key);
|
|
if (k.startsWith("auth:app_by_host:")) {
|
|
return Promise.resolve(
|
|
JSON.stringify({ id: "app-id-1", name: "test-app" }),
|
|
);
|
|
}
|
|
return Promise.resolve(
|
|
JSON.stringify({
|
|
uuid: "guest-id-1",
|
|
username: "guest_event_1",
|
|
account_status: "guest",
|
|
customScopes: ["app:other-app", "viewer"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
const req = new Request("http://localhost/api/forward-auth", {
|
|
headers: {
|
|
Cookie: "session_id=mock-guest-session",
|
|
"X-Forwarded-Host": "test.app.local",
|
|
},
|
|
});
|
|
|
|
const res = await app.request(req);
|
|
assertEquals(res.status, 403);
|
|
assertEquals(
|
|
await res.text(),
|
|
"Forbidden: Access denied to this application (Guest/Delegated)",
|
|
);
|
|
|
|
restoreMockSql();
|
|
valkeyStub.restore();
|
|
});
|
|
|
|
Deno.test("Tier 1 & 2: POST /api/guests/sandbox - Creates guest session", async () => {
|
|
const { valkey } = await import("../valkey.ts");
|
|
// Mock valkey.setex to prevent connection errors during tests
|
|
valkey.setex = async () => "OK" as any;
|
|
|
|
const req = new Request("http://localhost/api/guests/sandbox", {
|
|
method: "POST",
|
|
});
|
|
const res = await app.fetch(req);
|
|
assertEquals(res.status, 200);
|
|
const data = await res.json();
|
|
assertEquals(data.success, true);
|
|
assertEquals(true, !!data.sessionId);
|
|
assertEquals(true, !!data.guestUuid);
|
|
});
|