Compare commits
No commits in common. "f2310503f49309f420c2f637a8000b7c0f0b345b" and "e0c12bdef697663a4a59ba22bec4e67cd0b567b9" have entirely different histories.
f2310503f4
...
e0c12bdef6
1576
server/main.test.ts
Normal file
1576
server/main.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,403 +0,0 @@
|
||||
import { assert, 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 { auditWrapper } from "../audit.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("Phase 4: Audit Ledger Verification - Login failed", async () => {
|
||||
const mockUser = {
|
||||
id: "user-2",
|
||||
username: "bob",
|
||||
account_status: "suspended",
|
||||
};
|
||||
|
||||
let queryCount = 0;
|
||||
setMockSql(() => {
|
||||
queryCount++;
|
||||
if (queryCount === 1) {
|
||||
return Promise.resolve([{ user_id: "user-2" }]);
|
||||
} else {
|
||||
return Promise.resolve([mockUser]);
|
||||
}
|
||||
});
|
||||
|
||||
let auditArgs: any[] = [];
|
||||
const originalAudit = auditWrapper.auditLog;
|
||||
auditWrapper.auditLog = (...args: any[]) => {
|
||||
auditArgs = args;
|
||||
};
|
||||
|
||||
const req = new Request("http://localhost/api/login/verify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cookie": "expected_authentication_challenge=challenge",
|
||||
},
|
||||
body: JSON.stringify({ response: { id: "passkey-id" } }),
|
||||
});
|
||||
|
||||
const res = await app.request(req);
|
||||
assertEquals(res.status, 403);
|
||||
|
||||
assertEquals(auditArgs.length > 0, true);
|
||||
assertEquals(auditArgs[1], "login_failed");
|
||||
|
||||
restoreMockSql();
|
||||
auditWrapper.auditLog = originalAudit;
|
||||
});
|
||||
|
||||
Deno.test("WebAuthn - /api/register/verify extracts PRF", async () => {
|
||||
const { app } = await import("../main.ts");
|
||||
|
||||
const req = new Request("http://localhost/api/register/verify", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const res = await app.fetch(req);
|
||||
assertEquals(res.status, 400);
|
||||
const json = await res.json();
|
||||
assertEquals(json.error, "inviteCode or upgrade_session required");
|
||||
});
|
||||
|
||||
Deno.test("WebAuthn - /api/login/challenge handles username for PRF", async () => {
|
||||
const { app } = await import("../main.ts");
|
||||
const { sqlWrapper } = await import("../db.ts");
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
try {
|
||||
const mockSql = (strings: any, ..._values: any[]) => {
|
||||
const query = strings.join("?");
|
||||
if (query.includes("SELECT id FROM users WHERE username =")) {
|
||||
return Promise.resolve([{ id: "mock-user-id" }]);
|
||||
}
|
||||
if (query.includes("FROM passkeys WHERE user_id =")) {
|
||||
return Promise.resolve([{
|
||||
credential_id: "mock-cred",
|
||||
prf_enabled: true,
|
||||
prf_salt: "bW9jay1zYWx0", // "mock-salt"
|
||||
}]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
sqlWrapper.sql = mockSql as any;
|
||||
|
||||
const req = new Request("http://localhost/api/login/challenge", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: "testuser" }),
|
||||
});
|
||||
const res = await app.fetch(req);
|
||||
assertEquals(res.status, 200);
|
||||
|
||||
const json = await res.json();
|
||||
assertExists(json.options);
|
||||
assertExists(json.options.extensions);
|
||||
assertExists(json.options.extensions.prf);
|
||||
assertExists(json.options.extensions.prf.evalByCredential);
|
||||
assertExists(json.options.extensions.prf.evalByCredential["mock-cred"]);
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logout Return-Path Validation", async (t) => {
|
||||
await t.step("GET /logout preserves valid redirect", async () => {
|
||||
const req = new Request(
|
||||
"http://localhost/logout?redirect=https://ed-droid.atyg.org/",
|
||||
);
|
||||
const res = await app.fetch(req);
|
||||
assertEquals(res.status, 302);
|
||||
assertEquals(
|
||||
res.headers.get("Location"),
|
||||
"https://ed-droid.atyg.org/",
|
||||
);
|
||||
});
|
||||
|
||||
await t.step("GET /logout intercepts malicious redirect", async () => {
|
||||
const res = await app.request("/logout?redirect=https://evil.com", {
|
||||
method: "GET",
|
||||
});
|
||||
assertEquals(res.status, 302);
|
||||
const location = res.headers.get("location");
|
||||
assertExists(location);
|
||||
// Malicious redirect should be discarded, so it just redirects to /login
|
||||
assertEquals(location, "/login");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("App Launchpad Zero-Knowledge Query Logic", async (t) => {
|
||||
await t.step(
|
||||
"GET /dashboard redirects to /login if unauthenticated",
|
||||
async () => {
|
||||
const res = await app.request("/dashboard", { method: "GET" });
|
||||
assertEquals(res.status, 302);
|
||||
assertEquals(res.headers.get("location"), "/login");
|
||||
},
|
||||
);
|
||||
|
||||
await t.step("GET /dashboard renders for admin", async () => {
|
||||
const valkeyStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "valid_session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "admin-uuid", username: "admin" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = (strings: any, ..._values: any[]) => {
|
||||
const query = strings.join("?");
|
||||
if (query.includes("g.role = 'admin'")) {
|
||||
return Promise.resolve([{ id: "grant-id" }]);
|
||||
}
|
||||
if (
|
||||
query.includes("SELECT id, name, description, domain, 'Admin' as role")
|
||||
) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: "app1",
|
||||
name: "App 1",
|
||||
description: "Desc",
|
||||
domain: "app1.com",
|
||||
role: "Admin",
|
||||
},
|
||||
{
|
||||
id: "app2",
|
||||
name: "App 2",
|
||||
description: "Desc",
|
||||
domain: "app2.com",
|
||||
role: "Admin",
|
||||
},
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await app.request("/dashboard", {
|
||||
method: "GET",
|
||||
headers: { Cookie: "session_id=valid_session" },
|
||||
});
|
||||
assertEquals(res.status, 200);
|
||||
const text = await res.text();
|
||||
assert(text.includes("App 1"));
|
||||
assert(text.includes("App 2"));
|
||||
assert(text.includes("Admin Console")); // Layout link
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
await t.step("GET /dashboard renders for regular user", async () => {
|
||||
const valkeyStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "valid_session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "user-uuid", username: "user" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = (strings: any, ..._values: any[]) => {
|
||||
const query = strings.join("?");
|
||||
if (query.includes("g.role = 'admin'")) {
|
||||
return Promise.resolve([]); // Not admin
|
||||
}
|
||||
if (query.includes("ORDER BY created_at ASC LIMIT 1")) {
|
||||
return Promise.resolve([{ id: "different-user" }]);
|
||||
}
|
||||
if (
|
||||
query.includes("SELECT a.id, a.name, a.description, a.domain, g.role")
|
||||
) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: "app1",
|
||||
name: "App 1",
|
||||
description: "Desc",
|
||||
domain: "app1.com",
|
||||
role: "Viewer",
|
||||
},
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await app.request("/dashboard", {
|
||||
method: "GET",
|
||||
headers: { Cookie: "session_id=valid_session" },
|
||||
});
|
||||
assertEquals(res.status, 200);
|
||||
const text = await res.text();
|
||||
assert(text.includes("App 1"));
|
||||
assert(!text.includes("App 2"));
|
||||
assert(!text.includes("Admin Console")); // Layout link should be missing
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyStub.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Cookie Shadowing & Multi-Cookie Iteration", async (t) => {
|
||||
await t.step(
|
||||
"GET /dashboard authenticates when fresh session is shadowed by stale host-only cookie",
|
||||
async () => {
|
||||
const valkeyStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "fresh_wildcard_session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "user-uuid", username: "tylerg" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = () => Promise.resolve([]);
|
||||
|
||||
try {
|
||||
const res = await app.request("/dashboard", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
// Android Chrome sends stale host cookie first, then wildcard cookie
|
||||
Cookie:
|
||||
"session_id=stale_host_cookie; session_id=fresh_wildcard_session",
|
||||
},
|
||||
});
|
||||
assertEquals(res.status, 200);
|
||||
const text = await res.text();
|
||||
assert(text.includes("Application Launchpad"));
|
||||
|
||||
// Verify Set-Cookie header attempts to delete the host-only cookie
|
||||
const setCookieHeader = res.headers.get("set-cookie") || "";
|
||||
assert(
|
||||
setCookieHeader.includes("session_id=") &&
|
||||
(setCookieHeader.includes("Max-Age=0") ||
|
||||
setCookieHeader.includes("Expires=")),
|
||||
);
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Ephemeral 1-Click Magic Link Redemption (/pass)", async (t) => {
|
||||
await t.step("GET /pass with missing token redirects to login", async () => {
|
||||
const res = await app.request("/pass", { method: "GET" });
|
||||
assertEquals(res.status, 302);
|
||||
assertEquals(
|
||||
res.headers.get("location"),
|
||||
"/login?error=invalid_or_expired_pass",
|
||||
);
|
||||
});
|
||||
|
||||
await t.step("GET /pass with invalid token redirects to login", async () => {
|
||||
const valkeyStub = stub(valkey, "get", () => Promise.resolve(null));
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = (() => Promise.resolve([])) as any;
|
||||
try {
|
||||
const res = await app.request("/pass?token=invalid", { method: "GET" });
|
||||
assertEquals(res.status, 302);
|
||||
assertEquals(
|
||||
res.headers.get("location"),
|
||||
"/login?error=invalid_or_expired_pass",
|
||||
);
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
await t.step(
|
||||
"GET /pass with valid token sets cookies and redirects to app domain",
|
||||
async () => {
|
||||
const sessionToken = "ay_sess_valid_app";
|
||||
const valkeyGetStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === sessionToken) {
|
||||
return Promise.resolve(JSON.stringify({
|
||||
uuid: "user-uuid",
|
||||
username: "testuser",
|
||||
customScopes: ["app:ed-droid"],
|
||||
}));
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
const valkeyTtlStub = stub(valkey, "ttl", () => Promise.resolve(3600));
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
||||
const query = Array.isArray(strings)
|
||||
? strings.join("?")
|
||||
: String(strings);
|
||||
if (query.includes("SELECT domain FROM apps WHERE name =")) {
|
||||
return Promise.resolve([{ domain: "ed-droid.atyg.org" }]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}) as any;
|
||||
|
||||
try {
|
||||
const res = await app.request(`/pass?token=${sessionToken}`, {
|
||||
method: "GET",
|
||||
});
|
||||
assertEquals(res.status, 302);
|
||||
assertEquals(res.headers.get("location"), "https://ed-droid.atyg.org");
|
||||
|
||||
const cookies = res.headers.get("set-cookie");
|
||||
assertExists(cookies);
|
||||
assert(cookies.includes(`session_id=${sessionToken};`));
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyGetStub.restore();
|
||||
valkeyTtlStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"GET /pass with valid token defaults to /dashboard if no app scope",
|
||||
async () => {
|
||||
const sessionToken = "ay_sess_valid_dashboard";
|
||||
const valkeyGetStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === sessionToken) {
|
||||
return Promise.resolve(JSON.stringify({
|
||||
uuid: "user-uuid",
|
||||
username: "testuser",
|
||||
customScopes: ["read:audit"],
|
||||
}));
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
const valkeyTtlStub = stub(valkey, "ttl", () => Promise.resolve(3600));
|
||||
|
||||
try {
|
||||
const res = await app.request(`/pass?token=${sessionToken}`, {
|
||||
method: "GET",
|
||||
});
|
||||
assertEquals(res.status, 302);
|
||||
assertEquals(res.headers.get("location"), "/dashboard");
|
||||
} finally {
|
||||
valkeyGetStub.restore();
|
||||
valkeyTtlStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -1,297 +0,0 @@
|
||||
import { assert, 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";
|
||||
|
||||
rateLimitWrapper.checkRateLimit = () => Promise.resolve(true);
|
||||
|
||||
Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
||||
await t.step("POST /api/events creates an event pass", async () => {
|
||||
const valkeyStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "admin-session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "admin-uuid", username: "tylerg" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
||||
const query = Array.isArray(strings)
|
||||
? strings.join("?")
|
||||
: String(strings);
|
||||
if (query.includes("INSERT INTO event_passes")) {
|
||||
return Promise.resolve([{
|
||||
id: "event-uuid-1",
|
||||
slug: "deno-lab",
|
||||
pin_code: "749-123",
|
||||
name: "Deno Workshop",
|
||||
max_seats: 50,
|
||||
seats_claimed: 0,
|
||||
lifespan_hours: 3,
|
||||
}]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}) as any;
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/events", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer admin-session",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: "Deno Workshop",
|
||||
slug: "deno-lab",
|
||||
pinCode: "749-123",
|
||||
maxSeats: 50,
|
||||
lifespanHours: 3,
|
||||
}),
|
||||
});
|
||||
|
||||
assertEquals(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert(json.success === true);
|
||||
assertEquals(json.event.slug, "deno-lab");
|
||||
assertEquals(json.event.pin_code, "749-123");
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
await t.step(
|
||||
"POST /api/join redeems PIN / slug and mints guest session",
|
||||
async () => {
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
||||
const query = Array.isArray(strings)
|
||||
? strings.join("?")
|
||||
: String(strings);
|
||||
if (query.includes("UPDATE event_passes")) {
|
||||
return Promise.resolve([{
|
||||
id: "event-uuid-1",
|
||||
slug: "deno-lab",
|
||||
pin_code: "749-123",
|
||||
name: "Deno Workshop",
|
||||
max_seats: 50,
|
||||
seats_claimed: 1,
|
||||
lifespan_hours: 3,
|
||||
app_id: null,
|
||||
}]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}) as any;
|
||||
|
||||
const valkeySetexStub = stub(
|
||||
valkey,
|
||||
"setex",
|
||||
() => Promise.resolve("OK" as any),
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/join", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ code: "749-123" }),
|
||||
});
|
||||
|
||||
assertEquals(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert(json.success === true);
|
||||
assert(json.token.startsWith("ay_sess_"));
|
||||
assertEquals(json.username, "guest_deno-lab_1");
|
||||
|
||||
const cookies = res.headers.get("set-cookie");
|
||||
assertExists(cookies);
|
||||
assert(cookies.includes(`session_id=${json.token};`));
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeySetexStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"GET /join/:slug?format=env returns CLI export string",
|
||||
async () => {
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
||||
const query = Array.isArray(strings)
|
||||
? strings.join("?")
|
||||
: String(strings);
|
||||
if (query.includes("UPDATE event_passes")) {
|
||||
return Promise.resolve([{
|
||||
id: "event-uuid-1",
|
||||
slug: "deno-lab",
|
||||
pin_code: "749-123",
|
||||
name: "Deno Workshop",
|
||||
max_seats: 50,
|
||||
seats_claimed: 2,
|
||||
lifespan_hours: 3,
|
||||
app_id: null,
|
||||
}]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}) as any;
|
||||
|
||||
const valkeySetexStub = stub(
|
||||
valkey,
|
||||
"setex",
|
||||
() => Promise.resolve("OK" as any),
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await app.request("/join/deno-lab?format=env", {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
assertEquals(res.status, 200);
|
||||
const text = await res.text();
|
||||
assert(text.includes('export AUTH_YES_TOKEN="ay_sess_'));
|
||||
assert(text.includes('export AUTH_YES_USER="guest_deno-lab_2"'));
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeySetexStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"POST /api/events/:id/end master kill switch revokes sessions",
|
||||
async () => {
|
||||
const valkeyGetStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "admin-session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "admin-uuid", username: "tylerg" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
let updateEventCalled = false;
|
||||
let deleteSessionsCalled = false;
|
||||
|
||||
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
||||
const query = Array.isArray(strings)
|
||||
? strings.join("?")
|
||||
: String(strings);
|
||||
if (
|
||||
query.includes("UPDATE event_passes") &&
|
||||
query.includes("is_active = FALSE")
|
||||
) {
|
||||
updateEventCalled = true;
|
||||
return Promise.resolve([{ slug: "deno-lab" }]);
|
||||
}
|
||||
if (query.includes("DELETE FROM sessions")) {
|
||||
deleteSessionsCalled = true;
|
||||
return Promise.resolve([{ id: "sess-1" }, { id: "sess-2" }]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}) as any;
|
||||
|
||||
const valkeyDelStub = stub(
|
||||
valkey,
|
||||
"del",
|
||||
() => Promise.resolve(2 as any),
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/events/evt-123/end", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer admin-session",
|
||||
},
|
||||
});
|
||||
|
||||
assertEquals(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert(json.success === true);
|
||||
assertEquals(json.revokedCount, 2);
|
||||
assert(updateEventCalled);
|
||||
assert(deleteSessionsCalled);
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyGetStub.restore();
|
||||
valkeyDelStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"POST /api/events/:id/extend adds time to event and sessions",
|
||||
async () => {
|
||||
const valkeyGetStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "admin-session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "admin-uuid", username: "tylerg" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const futureDate = new Date(Date.now() + 7200000).toISOString();
|
||||
const originalSql = sqlWrapper.sql;
|
||||
let updateEventCalled = false;
|
||||
let updateSessionsCalled = false;
|
||||
|
||||
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
||||
const query = Array.isArray(strings)
|
||||
? strings.join("?")
|
||||
: String(strings);
|
||||
if (
|
||||
query.includes("UPDATE event_passes") &&
|
||||
query.includes("expires_at = expires_at + interval")
|
||||
) {
|
||||
updateEventCalled = true;
|
||||
return Promise.resolve([{
|
||||
slug: "deno-lab",
|
||||
expires_at: futureDate,
|
||||
}]);
|
||||
}
|
||||
if (
|
||||
query.includes("UPDATE sessions") &&
|
||||
query.includes("expires_at = expires_at + interval")
|
||||
) {
|
||||
updateSessionsCalled = true;
|
||||
return Promise.resolve([{ id: "sess-1" }, { id: "sess-2" }]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}) as any;
|
||||
|
||||
let expireCallCount = 0;
|
||||
const valkeyExpireStub = stub(valkey, "expire", () => {
|
||||
expireCallCount++;
|
||||
return Promise.resolve(1 as any);
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/events/evt-123/extend", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer admin-session",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ extendHours: 2 }),
|
||||
});
|
||||
|
||||
assertEquals(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert(json.success === true);
|
||||
assertEquals(json.expiresAt, futureDate);
|
||||
assert(updateEventCalled);
|
||||
assert(updateSessionsCalled);
|
||||
assertEquals(expireCallCount, 2);
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyGetStub.restore();
|
||||
valkeyExpireStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -1,285 +0,0 @@
|
||||
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: 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);
|
||||
});
|
||||
@ -1,155 +0,0 @@
|
||||
import { assertEquals } 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 { spireWrapper } from "../spire_ffi.ts";
|
||||
import { auditWrapper } from "../audit.ts";
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
|
||||
function setMockSql(mockImpl: () => Promise<any>) {
|
||||
sqlWrapper.sql = mockImpl as any;
|
||||
}
|
||||
|
||||
function restoreMockSql() {
|
||||
sqlWrapper.sql = originalSql;
|
||||
}
|
||||
|
||||
Deno.test("Tier 3: ValidateSession ConnectRPC - Default-Deny", async () => {
|
||||
const originalExtract = spireWrapper.extractSpiffeIdFromCert;
|
||||
spireWrapper.extractSpiffeIdFromCert = () => "spiffe://system.local/ed-droid";
|
||||
|
||||
let queryCount = 0;
|
||||
setMockSql(() => {
|
||||
queryCount++;
|
||||
if (queryCount === 1) {
|
||||
return Promise.resolve([{ id: "app-1" }]);
|
||||
} else {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
});
|
||||
|
||||
// Re-define valkey stub
|
||||
const valkeyStub = stub(valkey, "get", () => {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "user-1", username: "alice" }),
|
||||
);
|
||||
});
|
||||
|
||||
let auditCalled = false;
|
||||
const originalAudit = auditWrapper.auditLog;
|
||||
auditWrapper.auditLog = (..._args: any[]) => {
|
||||
auditCalled = true;
|
||||
};
|
||||
|
||||
const req = new Request(
|
||||
"http://localhost/auth.v1.AuthService/ValidateSession",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-peer-cert": "cert-data",
|
||||
},
|
||||
// The Connect protocol expects token under a specific format or we can just send it,
|
||||
// actually our handler looks at `req.token` via connect protocol, which translates to the json body for unary requests in Connect HTTP POST.
|
||||
// However, the `ValidateSession` requires an argument structured as `{ "token": "..." }`.
|
||||
body: JSON.stringify({ token: "some-session-token" }),
|
||||
},
|
||||
);
|
||||
|
||||
const res = await app.request(req);
|
||||
const data = await res.json();
|
||||
|
||||
// The data structure from universal handler might wrap the response.
|
||||
// Connect responses might look like just the json object.
|
||||
// We'll check if valid is false.
|
||||
|
||||
if (data.valid !== undefined) {
|
||||
assertEquals(data.valid, false);
|
||||
} else {
|
||||
// If not direct format, just ensure valid is falsy or check for error
|
||||
assertEquals(!!data.valid, false);
|
||||
}
|
||||
|
||||
assertEquals(auditCalled, true);
|
||||
|
||||
spireWrapper.extractSpiffeIdFromCert = originalExtract;
|
||||
restoreMockSql();
|
||||
valkeyStub.restore();
|
||||
auditWrapper.auditLog = originalAudit;
|
||||
});
|
||||
|
||||
Deno.test("Tier 3: ValidateSession ConnectRPC - Valid RBAC Grant", async () => {
|
||||
const originalExtract = spireWrapper.extractSpiffeIdFromCert;
|
||||
spireWrapper.extractSpiffeIdFromCert = () => "spiffe://system.local/ed-droid";
|
||||
|
||||
let queryCount = 0;
|
||||
setMockSql(() => {
|
||||
queryCount++;
|
||||
if (queryCount === 1) {
|
||||
return Promise.resolve([{ id: "app-1" }]);
|
||||
} else {
|
||||
return Promise.resolve([{ role: "viewer" }]);
|
||||
}
|
||||
});
|
||||
|
||||
const valkeyStub = stub(valkey, "get", () => {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "user-1", username: "alice" }),
|
||||
);
|
||||
});
|
||||
|
||||
const req = new Request(
|
||||
"http://localhost/auth.v1.AuthService/ValidateSession",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-peer-cert": "cert-data",
|
||||
},
|
||||
body: JSON.stringify({ token: "some-session-token" }),
|
||||
},
|
||||
);
|
||||
|
||||
const res = await app.request(req);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.valid !== undefined) {
|
||||
assertEquals(data.valid, true);
|
||||
assertEquals(data.uuid, "user-1");
|
||||
assertEquals(data.scopes[0], "viewer");
|
||||
}
|
||||
|
||||
spireWrapper.extractSpiffeIdFromCert = originalExtract;
|
||||
restoreMockSql();
|
||||
valkeyStub.restore();
|
||||
});
|
||||
|
||||
Deno.test("Tier 3: ValidateSession ConnectRPC - SPIFFE Attestation Failure", async () => {
|
||||
const originalExtract = spireWrapper.extractSpiffeIdFromCert;
|
||||
spireWrapper.extractSpiffeIdFromCert = () => null;
|
||||
|
||||
const req = new Request(
|
||||
"http://localhost/auth.v1.AuthService/ValidateSession",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-peer-cert": "invalid-cert",
|
||||
},
|
||||
body: JSON.stringify({ token: "some-session-token" }),
|
||||
},
|
||||
);
|
||||
|
||||
const res = await app.request(req);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.valid !== undefined) {
|
||||
assertEquals(data.valid, false);
|
||||
} else {
|
||||
assertEquals(!!data.valid, false);
|
||||
}
|
||||
|
||||
spireWrapper.extractSpiffeIdFromCert = originalExtract;
|
||||
});
|
||||
@ -1,395 +0,0 @@
|
||||
import { assert, assertEquals } 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("Zero-Trust Scope Guards", async (t) => {
|
||||
const mockUserId = "user-id-guards";
|
||||
|
||||
await t.step(
|
||||
"Delegated session is blocked from mutating sessions without scope",
|
||||
async () => {
|
||||
const mockSessionId = "ay_sess_delegated_123";
|
||||
|
||||
setMockSql(
|
||||
(async (strings: any, ..._values: any[]) => {
|
||||
const q = Array.isArray(strings)
|
||||
? strings.join("?")
|
||||
: String(strings);
|
||||
if (q.includes("SELECT id, is_agent FROM sessions")) {
|
||||
return [{ id: "other_session", is_agent: false }];
|
||||
}
|
||||
return [];
|
||||
}) as any,
|
||||
);
|
||||
|
||||
if ((valkey as any).get.restore) {
|
||||
(valkey as any).get.restore();
|
||||
}
|
||||
const valkeyStub = stub(
|
||||
valkey,
|
||||
"get",
|
||||
() =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
uuid: mockUserId,
|
||||
username: "agent-user",
|
||||
isAgent: true,
|
||||
customScopes: ["read:data"],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
const req = new Request(
|
||||
"http://localhost/api/sessions/other_session/extend",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${mockSessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ extendHours: 2 }),
|
||||
},
|
||||
);
|
||||
const res = await app.fetch(req);
|
||||
assertEquals(res.status, 403);
|
||||
const body = await res.json();
|
||||
assertEquals(body.error, "Forbidden: Insufficient scopes");
|
||||
} finally {
|
||||
valkeyStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step("Delegated session can self-revoke", async () => {
|
||||
const mockSessionId = "ay_sess_delegated_123";
|
||||
|
||||
setMockSql(
|
||||
(async (strings: any, ..._values: any[]) => {
|
||||
const q = Array.isArray(strings) ? strings.join("?") : String(strings);
|
||||
if (q.includes("SELECT id FROM sessions WHERE id =")) {
|
||||
return [{ id: mockSessionId }];
|
||||
}
|
||||
if (q.includes("DELETE FROM sessions")) {
|
||||
return [{ id: mockSessionId }];
|
||||
}
|
||||
return [];
|
||||
}) as any,
|
||||
);
|
||||
|
||||
if ((valkey as any).get.restore) {
|
||||
(valkey as any).get.restore();
|
||||
}
|
||||
const valkeyStub = stub(
|
||||
valkey,
|
||||
"get",
|
||||
() =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
uuid: mockUserId,
|
||||
username: "agent-user",
|
||||
isAgent: true,
|
||||
customScopes: ["read:data"],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const delStub = stub(valkey, "del", () => Promise.resolve(1 as any));
|
||||
|
||||
try {
|
||||
const req = new Request(
|
||||
`http://localhost/api/sessions/${mockSessionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": `Bearer ${mockSessionId}` },
|
||||
},
|
||||
);
|
||||
const res = await app.fetch(req);
|
||||
assertEquals(res.status, 200);
|
||||
const body = await res.json();
|
||||
assertEquals(body.success, true);
|
||||
} finally {
|
||||
valkeyStub.restore();
|
||||
delStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
await t.step(
|
||||
"Delegated session blocked from UI /admin redirect",
|
||||
async () => {
|
||||
const mockSessionId = "ay_sess_delegated_123";
|
||||
|
||||
setMockSql(
|
||||
(async (strings: any, ..._values: any[]) => {
|
||||
const q = Array.isArray(strings)
|
||||
? strings.join("?")
|
||||
: String(strings);
|
||||
if (q.includes("SELECT g.id")) {
|
||||
return [{ id: "admin-grant-id" }];
|
||||
}
|
||||
return [];
|
||||
}) as any,
|
||||
);
|
||||
|
||||
if ((valkey as any).get.restore) {
|
||||
(valkey as any).get.restore();
|
||||
}
|
||||
const valkeyStub = stub(
|
||||
valkey,
|
||||
"get",
|
||||
() =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
uuid: mockUserId,
|
||||
username: "agent-user",
|
||||
isAgent: true,
|
||||
customScopes: ["read:data"],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
const req = new Request("http://localhost/admin/users", {
|
||||
headers: { "Cookie": `session_id=${mockSessionId}` },
|
||||
});
|
||||
const res = await app.fetch(req);
|
||||
assertEquals(res.status, 302);
|
||||
assertEquals(res.headers.get("location"), "/dashboard");
|
||||
} finally {
|
||||
valkeyStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"Delegated session without admin scope blocked from POST /api/sessions/delegate",
|
||||
async () => {
|
||||
const mockSessionId = "ay_sess_delegated_123";
|
||||
|
||||
if ((valkey as any).get.restore) {
|
||||
(valkey as any).get.restore();
|
||||
}
|
||||
const valkeyStub = stub(
|
||||
valkey,
|
||||
"get",
|
||||
() =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
uuid: mockUserId,
|
||||
username: "agent-user",
|
||||
isAgent: true,
|
||||
customScopes: ["write:sessions"],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
const req = new Request("http://localhost/api/sessions/delegate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${mockSessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ label: "Sub Agent" }),
|
||||
});
|
||||
const res = await app.fetch(req);
|
||||
assertEquals(res.status, 403);
|
||||
const body = await res.json();
|
||||
assertEquals(body.error, "Forbidden: Insufficient scopes");
|
||||
} finally {
|
||||
valkeyStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"Delegated session without write:events blocked from POST /api/events",
|
||||
async () => {
|
||||
const mockSessionId = "ay_sess_delegated_123";
|
||||
|
||||
if ((valkey as any).get.restore) {
|
||||
(valkey as any).get.restore();
|
||||
}
|
||||
const valkeyStub = stub(
|
||||
valkey,
|
||||
"get",
|
||||
() =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
uuid: mockUserId,
|
||||
username: "agent-user",
|
||||
isAgent: true,
|
||||
customScopes: ["read:data"],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
const req = new Request("http://localhost/api/events", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${mockSessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ name: "Hacking Workshop" }),
|
||||
});
|
||||
const res = await app.fetch(req);
|
||||
assertEquals(res.status, 403);
|
||||
const body = await res.json();
|
||||
assertEquals(body.error, "Forbidden: Insufficient scopes");
|
||||
} finally {
|
||||
valkeyStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
restoreMockSql();
|
||||
});
|
||||
|
||||
Deno.test("Agent Session Delegation & Scoped Permissions", async (t) => {
|
||||
await t.step(
|
||||
"POST /api/sessions/delegate - Mints child session with scopes",
|
||||
async () => {
|
||||
const valkeyStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "admin-session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "admin-uuid", username: "tylerg" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
const valkeySetStub = stub(valkey, "set", () => Promise.resolve("OK"));
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = ((_query: any, ..._args: any[]) => {
|
||||
return Promise.resolve([{ id: "mock-id" }]);
|
||||
}) as any;
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/sessions/delegate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer admin-session",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
label: "Antigravity Assistant",
|
||||
lifespanHours: 12,
|
||||
mode: "read_only",
|
||||
}),
|
||||
});
|
||||
|
||||
assertEquals(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert(json.success === true);
|
||||
assert(json.sessionId.startsWith("ay_sess_"));
|
||||
assertEquals(json.label, "Antigravity Assistant");
|
||||
assert(json.scopes.includes("read:audit"));
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyStub.restore();
|
||||
valkeySetStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"PUT /api/sessions/:id/scopes - Updates custom scopes",
|
||||
async () => {
|
||||
const valkeyStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "admin-session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "admin-uuid", username: "tylerg" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = ((_query: any, ..._args: any[]) => {
|
||||
return Promise.resolve([{ id: "ay_sess_123", is_agent: true }]);
|
||||
}) as any;
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/sessions/ay_sess_123/scopes", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: "Bearer admin-session",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
customScopes: ["read:audit", "app:ed-droid"],
|
||||
}),
|
||||
});
|
||||
|
||||
assertEquals(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert(json.success === true);
|
||||
assertEquals(json.scopes, ["read:audit", "app:ed-droid"]);
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"POST /api/sessions/:id/extend - Extends session TTL",
|
||||
async () => {
|
||||
const valkeyStub = stub(valkey, "get", (key: any) => {
|
||||
if (String(key) === "admin-session") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ uuid: "admin-uuid", username: "tylerg" }),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
const valkeyExpireStub = stub(valkey, "expire", () => Promise.resolve(1));
|
||||
|
||||
const originalSql = sqlWrapper.sql;
|
||||
sqlWrapper.sql = ((_query: any, ..._args: any[]) => {
|
||||
return Promise.resolve([{
|
||||
id: "ay_sess_123",
|
||||
expires_at: new Date().toISOString(),
|
||||
}]);
|
||||
}) as any;
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/sessions/ay_sess_123/extend", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer admin-session",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
extendHours: 2,
|
||||
}),
|
||||
});
|
||||
|
||||
assertEquals(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert(json.success === true);
|
||||
assert(json.newExpiresAt);
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyStub.restore();
|
||||
valkeyExpireStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user