Compare commits

..

No commits in common. "aa6b731fcfa493a37fd7c7f259efd4bbf4f1be56" and "563723c6541137d60568f81ca0e19b4f2f693224" have entirely different histories.

8 changed files with 106 additions and 1235 deletions

View File

@ -1,854 +0,0 @@
diff --git a/server/auth-session.ts b/server/auth-session.ts
index f0814be..6c05e53 100644
--- a/server/auth-session.ts
+++ b/server/auth-session.ts
@@ -405,6 +405,75 @@ export function isSafeRedirectUrl(
/**
* Extracts the real client IP from X-Real-IP or X-Forwarded-For headers.
*/
+/**
+ * Evaluates if the current user's capabilities satisfy the required scope.
+ * If !auth.isAgent, primary sessions inherit full capabilities (returns true).
+ * If auth.isAgent, checks if customScopes contains the requiredScope or '*'.
+ */
+export function hasScope(
+ auth: AuthenticatedUser,
+ requiredScope: string,
+): boolean {
+ if (!auth.isAgent) return true;
+ if (!Array.isArray(auth.customScopes)) return false;
+ return auth.customScopes.includes("*") ||
+ auth.customScopes.includes(requiredScope);
+}
+
+/**
+ * Helper to check if the session itself is authorized as an admin.
+ */
+export async function isSessionAdmin(
+ auth: AuthenticatedUser,
+): Promise<boolean> {
+ const globalAdmin = await isGlobalAdmin(auth.userId);
+ if (!globalAdmin) return false;
+ if (!auth.isAgent) return true;
+ return Array.isArray(auth.customScopes) && auth.customScopes.includes("*");
+}
+
+/**
+ * Hono Middleware: Blocks access if the session is a delegated agent session.
+ */
+export async function requirePrimarySession(
+ c: Context,
+ next: () => Promise<void>,
+) {
+ const auth = await getAuthenticatedUser(c);
+ if (!auth) return c.json({ error: "Unauthorized" }, 401);
+ if (auth.isAgent) {
+ return c.json({ error: "Forbidden: Primary session required" }, 403);
+ }
+ await next();
+}
+
+/**
+ * Hono Middleware Factory: Requires a specific scope.
+ */
+export function requireScope(scope: string) {
+ return async (c: Context, next: () => Promise<void>) => {
+ const auth = await getAuthenticatedUser(c);
+ if (!auth) return c.json({ error: "Unauthorized" }, 401);
+ if (!hasScope(auth, scope)) {
+ return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
+ }
+ await next();
+ };
+}
+
+/**
+ * Hono Middleware: Blocks access unless the session is an admin session.
+ */
+export async function requireAdmin(c: Context, next: () => Promise<void>) {
+ const auth = await getAuthenticatedUser(c);
+ if (!auth) return c.json({ error: "Unauthorized" }, 401);
+ const isAdmin = await isSessionAdmin(auth);
+ if (!isAdmin) {
+ return c.json({ error: "Forbidden: Global admin access required" }, 403);
+ }
+ await next();
+}
+
export function getClientIp(c: Context): string {
const realIp = c.req.header("x-real-ip");
if (realIp) {
diff --git a/server/main.test.ts b/server/main.test.ts
index d5875b3..bf1e801 100644
--- a/server/main.test.ts
+++ b/server/main.test.ts
@@ -26,7 +26,7 @@ Deno.test("Tier 1 & 2: GET /api/forward-auth - Valid session", async () => {
id: "user-id-1",
username: "alice",
account_status: "active",
- };
+
setMockSql(() => Promise.resolve([mockUser]));
@@ -121,7 +121,7 @@ Deno.test("Ephemeral 1-Click Magic Link Redemption (/pass)", async (t) => {
return Promise.resolve([{ domain: "test-app.atyg.org" }]);
}
return Promise.resolve([]);
- };
+
try {
const res = await app.request(`/pass?token=${sessionToken}`, {
@@ -134,7 +134,7 @@ Deno.test("Ephemeral 1-Click Magic Link Redemption (/pass)", async (t) => {
const cookies = res.headers.get("set-cookie");
assertExists(cookies);
assert(cookies.includes("session_id=;")); // clear host-only cookie
- assert(cookies.includes(`session_id=${sessionToken};`)); // set wildcard cookie
+ assert(cookies.includes(`session_id=${sessionToken `)); // set wildcard cookie
// In tests, process.env.COOKIE_DOMAIN or getCookieDomain fallback doesn't output .atyg.org because rpID env variables may not be explicitly set in Deno tests. Wait, if it fails, let's just make sure it sets domain
} finally {
sqlWrapper.sql = originalSql;
@@ -254,7 +254,7 @@ Deno.test("Tier 1 & 2: GET /api/forward-auth - Suspended account", async () => {
id: "user-id-1",
username: "alice",
account_status: "suspended",
- };
+
setMockSql(() => Promise.resolve([mockUser]));
@@ -309,7 +309,7 @@ Deno.test("Tier 3: ValidateSession ConnectRPC - Default-Deny", async () => {
const originalAudit = auditWrapper.auditLog;
auditWrapper.auditLog = (..._args: any[]) => {
auditCalled = true;
- };
+
const req = new Request(
"http://localhost/auth.v1.AuthService/ValidateSession",
@@ -425,9 +425,9 @@ Deno.test("Tier 3: ValidateSession ConnectRPC - SPIFFE Attestation Failure", asy
Deno.test("Phase 4: Audit Ledger Verification - Login failed", async () => {
const mockUser = {
id: "user-2",
- username: "bob",
+
account_status: "suspended",
- };
+
let queryCount = 0;
setMockSql(() => {
@@ -443,7 +443,7 @@ Deno.test("Phase 4: Audit Ledger Verification - Login failed", async () => {
const originalAudit = auditWrapper.auditLog;
auditWrapper.auditLog = (...args: any[]) => {
auditArgs = args;
- };
+
const req = new Request("http://localhost/api/login/verify", {
method: "POST",
@@ -496,7 +496,7 @@ Deno.test("WebAuthn - /api/login/challenge handles username for PRF", async () =
}]);
}
return Promise.resolve([]);
- };
+
sqlWrapper.sql = mockSql as any;
const req = new Request("http://localhost/api/login/challenge", {
@@ -693,7 +693,7 @@ Deno.test("App Launchpad Zero-Knowledge Query Logic", async (t) => {
]);
}
return Promise.resolve([]);
- };
+
try {
const res = await app.request("/dashboard", {
@@ -744,7 +744,7 @@ Deno.test("App Launchpad Zero-Knowledge Query Logic", async (t) => {
]);
}
return Promise.resolve([]);
- };
+
try {
const res = await app.request("/dashboard", {
@@ -1003,7 +1003,7 @@ Deno.test("Ephemeral 1-Click Magic Link Redemption (/pass)", async (t) => {
const cookies = res.headers.get("set-cookie");
assertExists(cookies);
- assert(cookies.includes(`session_id=${sessionToken};`));
+ assert(cookies.includes(`session_id=${sessionToken `));
} finally {
sqlWrapper.sql = originalSql;
valkeyGetStub.restore();
@@ -1143,7 +1143,7 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
const cookies = res.headers.get("set-cookie");
assertExists(cookies);
- assert(cookies.includes(`session_id=${json.token};`));
+ assert(cookies.includes(`session_id=${json.token `));
} finally {
sqlWrapper.sql = originalSql;
valkeySetexStub.restore();
@@ -1330,3 +1330,127 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
},
);
});
+
+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 = strings.join("?");
+ 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();
+ }
+ stub(valkey, "get", () =>
+ Promise.resolve(JSON.stringify({
+ uuid: mockUserId,
+
+
+
+ })));
+
+ const req = new Request(
+ "http://localhost/api/sessions/other_session/extend",
+ {
+ method: "POST",
+ headers: { "Authorization": `Bearer ${mockSessionId}` },
+ },
+ );
+ const res = await app.fetch(req);
+ assertEquals(res.status, 403);
+ const body = await res.json();
+ assertEquals(body.error, "Forbidden: Insufficient scopes");
+ },
+ );
+
+ await t.step("Delegated session can self-revoke", async () => {
+ const mockSessionId = "ay_sess_delegated_123";
+
+ setMockSql(
+ (async (strings: any, ...values: any[]) => {
+ const q = strings.join("?");
+ if (q.includes("SELECT id FROM sessions WHERE id = ")) {
+ return [{ id: mockSessionId }];
+ }
+ return [];
+ }) as any,
+ );
+
+ if ((valkey as any).get.restore) {
+ (valkey as any).get.restore();
+ }
+ stub(valkey, "get", () =>
+ Promise.resolve(JSON.stringify({
+ uuid: mockUserId,
+
+
+
+ })));
+
+ stub(valkey, "del", () => Promise.resolve(1));
+
+ 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);
+ });
+
+ 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 = strings.join("?");
+ 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();
+ }
+ stub(valkey, "get", () =>
+ Promise.resolve(JSON.stringify({
+ uuid: mockUserId,
+
+
+
+ })));
+
+ const req = new Request("http://localhost/admin/users", {
+ headers: { "Authorization": `Bearer ${mockSessionId}` },
+ });
+ const res = await app.fetch(req);
+ assertEquals(res.status, 302);
+ assertEquals(res.headers.get("location"), "/dashboard");
+ },
+ );
+
+ restoreMockSql();
+});
diff --git a/server/main.ts b/server/main.ts
index 169f6e7..cb264ef 100644
--- a/server/main.ts
+++ b/server/main.ts
@@ -16,6 +16,8 @@ import {
extractAllSessionIds,
getAuthenticatedUser,
isGlobalAdmin,
+ requireAdmin,
+ requirePrimarySession,
} from "./auth-session.ts";
import {
decodeBase64Url,
@@ -175,6 +177,7 @@ app.use("/api/register/*", async (c, next) => {
});
// Admin Endpoints (/api/admin/*): 60 requests per minute by user_id
+app.use("/api/admin/*", requireAdmin);
app.use("/api/admin/*", async (c, next) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
@@ -193,6 +196,9 @@ app.use("/api/admin/*", async (c, next) => {
});
// ---------------------------------------------------------
+// Primary session requirement for passkeys management to prevent agent mutation
+app.use("/api/passkeys/*", requirePrimarySession);
+
// Provisioning & Registration (Use Cases 1, 2, 3)
// ---------------------------------------------------------
@@ -200,10 +206,6 @@ app.post("/api/admin/invites/create", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden: Global admin access required" }, 403);
- }
-
const {
appId,
role,
@@ -1048,10 +1050,6 @@ app.get("/api/admin/audit-logs", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden: Global admin access required" }, 403);
- }
-
const logs = await sqlWrapper.sql`
SELECT a.id, a.action, a.resource, a.details, a.ip_address, a.created_at, u.username as user
FROM audit_records a
@@ -1067,10 +1065,6 @@ app.get("/api/admin/users", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden: Global admin access required" }, 403);
- }
-
const users = await sqlWrapper.sql`
SELECT id, username, display_name, account_status
FROM users
@@ -1084,10 +1078,6 @@ app.post("/api/admin/users/:id/status", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden: Global admin access required" }, 403);
- }
-
const targetUserId = c.req.param("id");
const { status } = await c.req.json();
@@ -1114,10 +1104,6 @@ app.post("/api/admin/users/:id/profile", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden: Global admin access required" }, 403);
- }
-
const targetUserId = c.req.param("id");
const { displayName } = await c.req.json();
@@ -1143,9 +1129,6 @@ app.post("/api/admin/users/:id/profile", async (c) => {
app.get("/api/admin/apps", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const apps = await sqlWrapper.sql`
SELECT a.id, a.name, a.spiffe_id, a.description, a.created_at,
@@ -1161,9 +1144,6 @@ app.get("/api/admin/apps", async (c) => {
app.post("/api/admin/apps", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const {
name,
@@ -1207,9 +1187,6 @@ app.post("/api/admin/apps", async (c) => {
app.put("/api/admin/apps/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const appId = c.req.param("id");
const {
@@ -1253,9 +1230,6 @@ app.put("/api/admin/apps/:id", async (c) => {
app.delete("/api/admin/apps/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const appId = c.req.param("id");
const app = await sqlWrapper
@@ -1281,9 +1255,6 @@ app.delete("/api/admin/apps/:id", async (c) => {
app.get("/api/admin/roles", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const appId = c.req.query("appId");
let roles;
@@ -1312,9 +1283,6 @@ app.get("/api/admin/roles", async (c) => {
app.post("/api/admin/roles", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const { name, description, appId } = await c.req.json();
if (
@@ -1373,9 +1341,6 @@ app.post("/api/admin/roles", async (c) => {
app.put("/api/admin/roles/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const roleId = c.req.param("id");
const { name, description } = await c.req.json();
@@ -1410,9 +1375,6 @@ app.put("/api/admin/roles/:id", async (c) => {
app.delete("/api/admin/roles/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const roleId = c.req.param("id");
const role = await sqlWrapper
@@ -1445,9 +1407,6 @@ app.delete("/api/admin/roles/:id", async (c) => {
app.get("/api/admin/invites", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const invites = await sqlWrapper.sql`
SELECT i.id, i.code, i.role, i.max_uses, i.uses_count, i.auto_activate, i.expires_at, i.created_at, i.used_at,
@@ -1464,9 +1423,6 @@ app.get("/api/admin/invites", async (c) => {
app.get("/api/admin/invites/:id/redemptions", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const inviteId = c.req.param("id");
const redemptions = await sqlWrapper.sql`
@@ -1483,9 +1439,6 @@ app.get("/api/admin/invites/:id/redemptions", async (c) => {
app.delete("/api/admin/invites/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const inviteId = c.req.param("id");
const invite = await sqlWrapper
@@ -1511,9 +1464,6 @@ app.delete("/api/admin/invites/:id", async (c) => {
app.get("/api/admin/users/:id/grants", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const targetUserId = c.req.param("id");
const grants = await sqlWrapper.sql`
@@ -1529,9 +1479,6 @@ app.get("/api/admin/users/:id/grants", async (c) => {
app.post("/api/admin/users/:id/grants", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const targetUserId = c.req.param("id");
const { appId, role } = await c.req.json();
@@ -1572,9 +1519,6 @@ app.post("/api/admin/users/:id/grants", async (c) => {
app.delete("/api/admin/users/:id/grants/:appId", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const { id: targetUserId, appId } = c.req.param();
@@ -1605,9 +1549,6 @@ app.delete("/api/admin/users/:id/grants/:appId", async (c) => {
app.get("/api/admin/aaguid", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const allowlist = await sqlWrapper
.sql`SELECT id, aaguid, description, created_at FROM aaguid_allowlist ORDER BY created_at DESC`;
return c.json({ allowlist });
@@ -1616,9 +1557,6 @@ app.get("/api/admin/aaguid", async (c) => {
app.post("/api/admin/aaguid", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const { aaguid, description } = await c.req.json();
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i;
@@ -1649,9 +1587,6 @@ app.post("/api/admin/aaguid", async (c) => {
app.delete("/api/admin/aaguid/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const id = c.req.param("id");
const record = await sqlWrapper
.sql`DELETE FROM aaguid_allowlist WHERE id = ${id} RETURNING aaguid`
@@ -1675,9 +1610,6 @@ app.delete("/api/admin/aaguid/:id", async (c) => {
app.post("/api/admin/hwk", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const { jwk, name } = await c.req.json();
if (!jwk || !name || typeof name !== "string") {
@@ -1721,9 +1653,6 @@ app.post("/api/admin/hwk", async (c) => {
app.delete("/api/admin/hwk/:fingerprint", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const fingerprint = c.req.param("fingerprint");
@@ -1755,9 +1684,6 @@ app.delete("/api/admin/hwk/:fingerprint", async (c) => {
app.get("/api/admin/users/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const targetUserId = c.req.param("id");
const user = await sqlWrapper
.sql`SELECT id, username, display_name, account_status FROM users WHERE id = ${targetUserId}`
@@ -1773,9 +1699,6 @@ app.get("/api/admin/users/:id", async (c) => {
app.delete("/api/admin/sessions/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const sessionId = c.req.param("id");
const session = await sqlWrapper
.sql`DELETE FROM sessions WHERE id = ${sessionId} RETURNING user_id`
@@ -1800,9 +1723,6 @@ app.delete("/api/admin/sessions/:id", async (c) => {
app.delete("/api/admin/users/:id/sessions", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const targetUserId = c.req.param("id");
const sessions = await sqlWrapper
.sql`DELETE FROM sessions WHERE user_id = ${targetUserId} RETURNING id`;
@@ -1824,9 +1744,6 @@ app.delete("/api/admin/users/:id/sessions", async (c) => {
app.delete("/api/admin/users/:userId/passkeys/:passkeyId", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const { userId, passkeyId } = c.req.param();
const passkey = await sqlWrapper
.sql`DELETE FROM passkeys WHERE id = ${passkeyId} AND user_id = ${userId} RETURNING id`
@@ -1847,9 +1764,6 @@ app.delete("/api/admin/users/:userId/passkeys/:passkeyId", async (c) => {
app.post("/api/admin/users/:id/recovery", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
- if (!(await isGlobalAdmin(auth.userId))) {
- return c.json({ error: "Forbidden" }, 403);
- }
const targetUserId = c.req.param("id");
const targetUser = await sqlWrapper
.sql`SELECT id FROM users WHERE id = ${targetUserId}`
diff --git a/server/routes/events.ts b/server/routes/events.ts
index ea9944a..c71eacc 100644
--- a/server/routes/events.ts
+++ b/server/routes/events.ts
@@ -3,7 +3,11 @@ import { deleteCookie, setCookie } from "jsr:@hono/hono@4/cookie";
import { encodeHex } from "jsr:@std/encoding@1/hex";
import { sqlWrapper } from "../db.ts";
import { valkey } from "../valkey.ts";
-import { getAuthenticatedUser, getCookieDomain } from "../auth-session.ts";
+import {
+ getAuthenticatedUser,
+ getCookieDomain,
+ hasScope,
+} from "../auth-session.ts";
import { EventJoinPage } from "../../ui/components/EventJoinPage.tsx";
import { EventSplashPage } from "../../ui/components/EventSplashPage.tsx";
@@ -17,6 +21,10 @@ eventRoutes.post("/api/events/:id/end", async (c) => {
const user = await getAuthenticatedUser(c);
if (!user) return c.json({ error: "Unauthorized" }, 401);
+ if (!hasScope(user, "write:events")) {
+ return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
+ }
+
const eventId = c.req.param("id");
try {
@@ -57,6 +65,10 @@ eventRoutes.post("/api/events/:id/extend", async (c) => {
const user = await getAuthenticatedUser(c);
if (!user) return c.json({ error: "Unauthorized" }, 401);
+ if (!hasScope(user, "write:events")) {
+ return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
+ }
+
const eventId = c.req.param("id");
const body = await c.req.json().catch(() => ({}));
const extendHours = Math.max(Number(body.extendHours) || 1, 1);
@@ -110,6 +122,10 @@ eventRoutes.post("/api/events", async (c) => {
const user = await getAuthenticatedUser(c);
if (!user) return c.json({ error: "Unauthorized" }, 401);
+ if (!hasScope(user, "write:events")) {
+ return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
+ }
+
const body = await c.req.json().catch(() => ({}));
const { name, appId, role = "viewer", maxSeats = 50, lifespanHours = 3 } =
body;
diff --git a/server/routes/sessions.ts b/server/routes/sessions.ts
index 11a9cd7..d2e9816 100644
--- a/server/routes/sessions.ts
+++ b/server/routes/sessions.ts
@@ -2,7 +2,12 @@ import { Hono } from "jsr:@hono/hono@4";
import { sqlWrapper } from "../db.ts";
import { valkey } from "../valkey.ts";
import { auditWrapper } from "../audit.ts";
-import { getAuthenticatedUser, getClientIp } from "../auth-session.ts";
+import {
+ getAuthenticatedUser,
+ getClientIp,
+ hasScope,
+
+} from "../auth-session.ts";
export const sessionRoutes = new Hono();
@@ -32,6 +37,13 @@ sessionRoutes.post("/api/sessions/delegate", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
+ // If delegating, the current session must have 'admin' scope or '*' if it's an agent
+ if (auth.isAgent) {
+ if (!hasScope(auth, "admin") && !hasScope(auth, "*")) {
+ return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
+ }
+ }
+
const {
label,
lifespanHours = 1,
@@ -119,6 +131,10 @@ sessionRoutes.put("/api/sessions/:id/scopes", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
+ if (!hasScope(auth, "write:sessions")) {
+ return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
+ }
+
const targetSessionId = c.req.param("id");
const { customScopes = [] } = await c.req.json();
@@ -168,6 +184,10 @@ sessionRoutes.post("/api/sessions/:id/extend", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
+ if (!hasScope(auth, "write:sessions")) {
+ return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
+ }
+
const targetSessionId = c.req.param("id");
const { extendHours = 1 } = await c.req.json();
const additionalHours = Math.max(Number(extendHours) || 1, 1);
@@ -215,6 +235,12 @@ sessionRoutes.delete("/api/sessions/:id", async (c) => {
const targetSessionId = c.req.param("id");
+ if (targetSessionId !== auth.sessionId) {
+ if (!hasScope(auth, "write:sessions")) {
+ return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
+ }
+ }
+
const session = await sqlWrapper.sql`
SELECT id FROM sessions WHERE id = ${targetSessionId} AND user_id = ${auth.userId}
`.then((res: any) => res[0]);
diff --git a/tasks/new/2026-0825.01.jul.sec.auth-api.zero-trust-scope-guards-1805.md b/tasks/wip/2026-0825.01.jul.sec.auth-api.zero-trust-scope-guards-1805.md
similarity index 100%
rename from tasks/new/2026-0825.01.jul.sec.auth-api.zero-trust-scope-guards-1805.md
rename to tasks/wip/2026-0825.01.jul.sec.auth-api.zero-trust-scope-guards-1805.md
diff --git a/ui/mod.ts b/ui/mod.ts
index b98b99a..6a758ea 100644
--- a/ui/mod.ts
+++ b/ui/mod.ts
@@ -9,6 +9,7 @@ import {
getCookieDomain,
isGlobalAdmin,
isSafeRedirectUrl,
+ isSessionAdmin,
} from "../server/auth-session.ts";
import { auditWrapper } from "../server/audit.ts";
import { LoginPage } from "./components/LoginPage.tsx";
@@ -258,8 +259,9 @@ uiApp.get("/admin/users", async (c) => {
}
const isAdmin = await isGlobalAdmin(auth.userId);
- if (!isAdmin) {
- return c.redirect("/dashboard");
+ const isSessionAdminRole = await isSessionAdmin(auth);
+ if (!isAdmin || !isSessionAdminRole) {
+ return c.redirect("/dashboard", 302);
}
const users = await sql`
@@ -286,8 +288,9 @@ uiApp.get("/admin/apps", async (c) => {
}
const isAdmin = await isGlobalAdmin(auth.userId);
- if (!isAdmin) {
- return c.redirect("/dashboard");
+ const isSessionAdminRole = await isSessionAdmin(auth);
+ if (!isAdmin || !isSessionAdminRole) {
+ return c.redirect("/dashboard", 302);
}
const apps = await sql`
@@ -317,8 +320,9 @@ uiApp.get("/admin/roles", async (c) => {
}
const isAdmin = await isGlobalAdmin(auth.userId);
- if (!isAdmin) {
- return c.redirect("/dashboard");
+ const isSessionAdminRole = await isSessionAdmin(auth);
+ if (!isAdmin || !isSessionAdminRole) {
+ return c.redirect("/dashboard", 302);
}
const roles = await sql`
@@ -351,8 +355,9 @@ uiApp.get("/admin/invites", async (c) => {
}
const isAdmin = await isGlobalAdmin(auth.userId);
- if (!isAdmin) {
- return c.redirect("/dashboard");
+ const isSessionAdminRole = await isSessionAdmin(auth);
+ if (!isAdmin || !isSessionAdminRole) {
+ return c.redirect("/dashboard", 302);
}
const invites = await sql`
@@ -419,8 +424,9 @@ uiApp.get("/admin/users/:id", async (c) => {
}
const isAdmin = await isGlobalAdmin(auth.userId);
- if (!isAdmin) {
- return c.redirect("/admin/users");
+ const isSessionAdminRole = await isSessionAdmin(auth);
+ if (!isAdmin || !isSessionAdminRole) {
+ return c.redirect("/admin/users", 302);
}
const targetUserId = c.req.param("id");
@@ -490,8 +496,9 @@ uiApp.get("/admin/audit-logs", async (c) => {
}
const isAdmin = await isGlobalAdmin(auth.userId);
- if (!isAdmin) {
- return c.redirect("/dashboard");
+ const isSessionAdminRole = await isSessionAdmin(auth);
+ if (!isAdmin || !isSessionAdminRole) {
+ return c.redirect("/dashboard", 302);
}
const logs = await sql`

View File

@ -405,75 +405,6 @@ export function isSafeRedirectUrl(
/** /**
* Extracts the real client IP from X-Real-IP or X-Forwarded-For headers. * Extracts the real client IP from X-Real-IP or X-Forwarded-For headers.
*/ */
/**
* Evaluates if the current user's capabilities satisfy the required scope.
* If !auth.isAgent, primary sessions inherit full capabilities (returns true).
* If auth.isAgent, checks if customScopes contains the requiredScope or '*'.
*/
export function hasScope(
auth: AuthenticatedUser,
requiredScope: string,
): boolean {
if (!auth.isAgent) return true;
if (!Array.isArray(auth.customScopes)) return false;
return auth.customScopes.includes("*") ||
auth.customScopes.includes(requiredScope);
}
/**
* Helper to check if the session itself is authorized as an admin.
*/
export async function isSessionAdmin(
auth: AuthenticatedUser,
): Promise<boolean> {
const globalAdmin = await isGlobalAdmin(auth.userId);
if (!globalAdmin) return false;
if (!auth.isAgent) return true;
return Array.isArray(auth.customScopes) && auth.customScopes.includes("*");
}
/**
* Hono Middleware: Blocks access if the session is a delegated agent session.
*/
export async function requirePrimarySession(
c: Context,
next: () => Promise<void>,
) {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (auth.isAgent) {
return c.json({ error: "Forbidden: Primary session required" }, 403);
}
await next();
}
/**
* Hono Middleware Factory: Requires a specific scope.
*/
export function requireScope(scope: string) {
return async (c: Context, next: () => Promise<void>) => {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(auth, scope)) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
await next();
};
}
/**
* Hono Middleware: Blocks access unless the session is an admin session.
*/
export async function requireAdmin(c: Context, next: () => Promise<void>) {
const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401);
const isAdmin = await isSessionAdmin(auth);
if (!isAdmin) {
return c.json({ error: "Forbidden: Global admin access required" }, 403);
}
await next();
}
export function getClientIp(c: Context): string { export function getClientIp(c: Context): string {
const realIp = c.req.header("x-real-ip"); const realIp = c.req.header("x-real-ip");
if (realIp) { if (realIp) {

View File

@ -1330,247 +1330,3 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
}, },
); );
}); });
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();
});

View File

@ -16,8 +16,6 @@ import {
extractAllSessionIds, extractAllSessionIds,
getAuthenticatedUser, getAuthenticatedUser,
isGlobalAdmin, isGlobalAdmin,
requireAdmin,
requirePrimarySession,
} from "./auth-session.ts"; } from "./auth-session.ts";
import { import {
decodeBase64Url, decodeBase64Url,
@ -177,7 +175,6 @@ app.use("/api/register/*", async (c, next) => {
}); });
// Admin Endpoints (/api/admin/*): 60 requests per minute by user_id // Admin Endpoints (/api/admin/*): 60 requests per minute by user_id
app.use("/api/admin/*", requireAdmin);
app.use("/api/admin/*", async (c, next) => { app.use("/api/admin/*", async (c, next) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) { if (!auth) {
@ -196,9 +193,6 @@ app.use("/api/admin/*", async (c, next) => {
}); });
// --------------------------------------------------------- // ---------------------------------------------------------
// Primary session requirement for passkeys management to prevent agent mutation
app.use("/api/passkeys/*", requirePrimarySession);
// Provisioning & Registration (Use Cases 1, 2, 3) // Provisioning & Registration (Use Cases 1, 2, 3)
// --------------------------------------------------------- // ---------------------------------------------------------
@ -206,6 +200,10 @@ app.post("/api/admin/invites/create", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden: Global admin access required" }, 403);
}
const { const {
appId, appId,
role, role,
@ -1050,6 +1048,10 @@ app.get("/api/admin/audit-logs", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden: Global admin access required" }, 403);
}
const logs = await sqlWrapper.sql` const logs = await sqlWrapper.sql`
SELECT a.id, a.action, a.resource, a.details, a.ip_address, a.created_at, u.username as user SELECT a.id, a.action, a.resource, a.details, a.ip_address, a.created_at, u.username as user
FROM audit_records a FROM audit_records a
@ -1065,6 +1067,10 @@ app.get("/api/admin/users", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden: Global admin access required" }, 403);
}
const users = await sqlWrapper.sql` const users = await sqlWrapper.sql`
SELECT id, username, display_name, account_status SELECT id, username, display_name, account_status
FROM users FROM users
@ -1078,6 +1084,10 @@ app.post("/api/admin/users/:id/status", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden: Global admin access required" }, 403);
}
const targetUserId = c.req.param("id"); const targetUserId = c.req.param("id");
const { status } = await c.req.json(); const { status } = await c.req.json();
@ -1104,6 +1114,10 @@ app.post("/api/admin/users/:id/profile", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden: Global admin access required" }, 403);
}
const targetUserId = c.req.param("id"); const targetUserId = c.req.param("id");
const { displayName } = await c.req.json(); const { displayName } = await c.req.json();
@ -1129,6 +1143,9 @@ app.post("/api/admin/users/:id/profile", async (c) => {
app.get("/api/admin/apps", async (c) => { app.get("/api/admin/apps", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const apps = await sqlWrapper.sql` const apps = await sqlWrapper.sql`
SELECT a.id, a.name, a.spiffe_id, a.description, a.created_at, SELECT a.id, a.name, a.spiffe_id, a.description, a.created_at,
@ -1144,6 +1161,9 @@ app.get("/api/admin/apps", async (c) => {
app.post("/api/admin/apps", async (c) => { app.post("/api/admin/apps", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const { const {
name, name,
@ -1187,6 +1207,9 @@ app.post("/api/admin/apps", async (c) => {
app.put("/api/admin/apps/:id", async (c) => { app.put("/api/admin/apps/:id", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const appId = c.req.param("id"); const appId = c.req.param("id");
const { const {
@ -1230,6 +1253,9 @@ app.put("/api/admin/apps/:id", async (c) => {
app.delete("/api/admin/apps/:id", async (c) => { app.delete("/api/admin/apps/:id", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const appId = c.req.param("id"); const appId = c.req.param("id");
const app = await sqlWrapper const app = await sqlWrapper
@ -1255,6 +1281,9 @@ app.delete("/api/admin/apps/:id", async (c) => {
app.get("/api/admin/roles", async (c) => { app.get("/api/admin/roles", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const appId = c.req.query("appId"); const appId = c.req.query("appId");
let roles; let roles;
@ -1283,6 +1312,9 @@ app.get("/api/admin/roles", async (c) => {
app.post("/api/admin/roles", async (c) => { app.post("/api/admin/roles", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const { name, description, appId } = await c.req.json(); const { name, description, appId } = await c.req.json();
if ( if (
@ -1341,6 +1373,9 @@ app.post("/api/admin/roles", async (c) => {
app.put("/api/admin/roles/:id", async (c) => { app.put("/api/admin/roles/:id", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const roleId = c.req.param("id"); const roleId = c.req.param("id");
const { name, description } = await c.req.json(); const { name, description } = await c.req.json();
@ -1375,6 +1410,9 @@ app.put("/api/admin/roles/:id", async (c) => {
app.delete("/api/admin/roles/:id", async (c) => { app.delete("/api/admin/roles/:id", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const roleId = c.req.param("id"); const roleId = c.req.param("id");
const role = await sqlWrapper const role = await sqlWrapper
@ -1407,6 +1445,9 @@ app.delete("/api/admin/roles/:id", async (c) => {
app.get("/api/admin/invites", async (c) => { app.get("/api/admin/invites", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const invites = await sqlWrapper.sql` const invites = await sqlWrapper.sql`
SELECT i.id, i.code, i.role, i.max_uses, i.uses_count, i.auto_activate, i.expires_at, i.created_at, i.used_at, SELECT i.id, i.code, i.role, i.max_uses, i.uses_count, i.auto_activate, i.expires_at, i.created_at, i.used_at,
@ -1423,6 +1464,9 @@ app.get("/api/admin/invites", async (c) => {
app.get("/api/admin/invites/:id/redemptions", async (c) => { app.get("/api/admin/invites/:id/redemptions", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const inviteId = c.req.param("id"); const inviteId = c.req.param("id");
const redemptions = await sqlWrapper.sql` const redemptions = await sqlWrapper.sql`
@ -1439,6 +1483,9 @@ app.get("/api/admin/invites/:id/redemptions", async (c) => {
app.delete("/api/admin/invites/:id", async (c) => { app.delete("/api/admin/invites/:id", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const inviteId = c.req.param("id"); const inviteId = c.req.param("id");
const invite = await sqlWrapper const invite = await sqlWrapper
@ -1464,6 +1511,9 @@ app.delete("/api/admin/invites/:id", async (c) => {
app.get("/api/admin/users/:id/grants", async (c) => { app.get("/api/admin/users/:id/grants", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const targetUserId = c.req.param("id"); const targetUserId = c.req.param("id");
const grants = await sqlWrapper.sql` const grants = await sqlWrapper.sql`
@ -1479,6 +1529,9 @@ app.get("/api/admin/users/:id/grants", async (c) => {
app.post("/api/admin/users/:id/grants", async (c) => { app.post("/api/admin/users/:id/grants", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const targetUserId = c.req.param("id"); const targetUserId = c.req.param("id");
const { appId, role } = await c.req.json(); const { appId, role } = await c.req.json();
@ -1519,6 +1572,9 @@ app.post("/api/admin/users/:id/grants", async (c) => {
app.delete("/api/admin/users/:id/grants/:appId", async (c) => { app.delete("/api/admin/users/:id/grants/:appId", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const { id: targetUserId, appId } = c.req.param(); const { id: targetUserId, appId } = c.req.param();
@ -1549,6 +1605,9 @@ app.delete("/api/admin/users/:id/grants/:appId", async (c) => {
app.get("/api/admin/aaguid", async (c) => { app.get("/api/admin/aaguid", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const allowlist = await sqlWrapper const allowlist = await sqlWrapper
.sql`SELECT id, aaguid, description, created_at FROM aaguid_allowlist ORDER BY created_at DESC`; .sql`SELECT id, aaguid, description, created_at FROM aaguid_allowlist ORDER BY created_at DESC`;
return c.json({ allowlist }); return c.json({ allowlist });
@ -1557,6 +1616,9 @@ app.get("/api/admin/aaguid", async (c) => {
app.post("/api/admin/aaguid", async (c) => { app.post("/api/admin/aaguid", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const { aaguid, description } = await c.req.json(); const { aaguid, description } = await c.req.json();
const uuidRegex = const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i; /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i;
@ -1587,6 +1649,9 @@ app.post("/api/admin/aaguid", async (c) => {
app.delete("/api/admin/aaguid/:id", async (c) => { app.delete("/api/admin/aaguid/:id", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const id = c.req.param("id"); const id = c.req.param("id");
const record = await sqlWrapper const record = await sqlWrapper
.sql`DELETE FROM aaguid_allowlist WHERE id = ${id} RETURNING aaguid` .sql`DELETE FROM aaguid_allowlist WHERE id = ${id} RETURNING aaguid`
@ -1610,6 +1675,9 @@ app.delete("/api/admin/aaguid/:id", async (c) => {
app.post("/api/admin/hwk", async (c) => { app.post("/api/admin/hwk", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const { jwk, name } = await c.req.json(); const { jwk, name } = await c.req.json();
if (!jwk || !name || typeof name !== "string") { if (!jwk || !name || typeof name !== "string") {
@ -1653,6 +1721,9 @@ app.post("/api/admin/hwk", async (c) => {
app.delete("/api/admin/hwk/:fingerprint", async (c) => { app.delete("/api/admin/hwk/:fingerprint", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const fingerprint = c.req.param("fingerprint"); const fingerprint = c.req.param("fingerprint");
@ -1684,6 +1755,9 @@ app.delete("/api/admin/hwk/:fingerprint", async (c) => {
app.get("/api/admin/users/:id", async (c) => { app.get("/api/admin/users/:id", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const targetUserId = c.req.param("id"); const targetUserId = c.req.param("id");
const user = await sqlWrapper const user = await sqlWrapper
.sql`SELECT id, username, display_name, account_status FROM users WHERE id = ${targetUserId}` .sql`SELECT id, username, display_name, account_status FROM users WHERE id = ${targetUserId}`
@ -1699,6 +1773,9 @@ app.get("/api/admin/users/:id", async (c) => {
app.delete("/api/admin/sessions/:id", async (c) => { app.delete("/api/admin/sessions/:id", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const sessionId = c.req.param("id"); const sessionId = c.req.param("id");
const session = await sqlWrapper const session = await sqlWrapper
.sql`DELETE FROM sessions WHERE id = ${sessionId} RETURNING user_id` .sql`DELETE FROM sessions WHERE id = ${sessionId} RETURNING user_id`
@ -1723,6 +1800,9 @@ app.delete("/api/admin/sessions/:id", async (c) => {
app.delete("/api/admin/users/:id/sessions", async (c) => { app.delete("/api/admin/users/:id/sessions", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const targetUserId = c.req.param("id"); const targetUserId = c.req.param("id");
const sessions = await sqlWrapper const sessions = await sqlWrapper
.sql`DELETE FROM sessions WHERE user_id = ${targetUserId} RETURNING id`; .sql`DELETE FROM sessions WHERE user_id = ${targetUserId} RETURNING id`;
@ -1744,6 +1824,9 @@ app.delete("/api/admin/users/:id/sessions", async (c) => {
app.delete("/api/admin/users/:userId/passkeys/:passkeyId", async (c) => { app.delete("/api/admin/users/:userId/passkeys/:passkeyId", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const { userId, passkeyId } = c.req.param(); const { userId, passkeyId } = c.req.param();
const passkey = await sqlWrapper const passkey = await sqlWrapper
.sql`DELETE FROM passkeys WHERE id = ${passkeyId} AND user_id = ${userId} RETURNING id` .sql`DELETE FROM passkeys WHERE id = ${passkeyId} AND user_id = ${userId} RETURNING id`
@ -1764,6 +1847,9 @@ app.delete("/api/admin/users/:userId/passkeys/:passkeyId", async (c) => {
app.post("/api/admin/users/:id/recovery", async (c) => { app.post("/api/admin/users/:id/recovery", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!(await isGlobalAdmin(auth.userId))) {
return c.json({ error: "Forbidden" }, 403);
}
const targetUserId = c.req.param("id"); const targetUserId = c.req.param("id");
const targetUser = await sqlWrapper const targetUser = await sqlWrapper
.sql`SELECT id FROM users WHERE id = ${targetUserId}` .sql`SELECT id FROM users WHERE id = ${targetUserId}`

View File

@ -3,11 +3,7 @@ import { deleteCookie, setCookie } from "jsr:@hono/hono@4/cookie";
import { encodeHex } from "jsr:@std/encoding@1/hex"; import { encodeHex } from "jsr:@std/encoding@1/hex";
import { sqlWrapper } from "../db.ts"; import { sqlWrapper } from "../db.ts";
import { valkey } from "../valkey.ts"; import { valkey } from "../valkey.ts";
import { import { getAuthenticatedUser, getCookieDomain } from "../auth-session.ts";
getAuthenticatedUser,
getCookieDomain,
hasScope,
} from "../auth-session.ts";
import { EventJoinPage } from "../../ui/components/EventJoinPage.tsx"; import { EventJoinPage } from "../../ui/components/EventJoinPage.tsx";
import { EventSplashPage } from "../../ui/components/EventSplashPage.tsx"; import { EventSplashPage } from "../../ui/components/EventSplashPage.tsx";
@ -21,10 +17,6 @@ eventRoutes.post("/api/events/:id/end", async (c) => {
const user = await getAuthenticatedUser(c); const user = await getAuthenticatedUser(c);
if (!user) return c.json({ error: "Unauthorized" }, 401); if (!user) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(user, "write:events")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
const eventId = c.req.param("id"); const eventId = c.req.param("id");
try { try {
@ -65,10 +57,6 @@ eventRoutes.post("/api/events/:id/extend", async (c) => {
const user = await getAuthenticatedUser(c); const user = await getAuthenticatedUser(c);
if (!user) return c.json({ error: "Unauthorized" }, 401); if (!user) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(user, "write:events")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
const eventId = c.req.param("id"); const eventId = c.req.param("id");
const body = await c.req.json().catch(() => ({})); const body = await c.req.json().catch(() => ({}));
const extendHours = Math.max(Number(body.extendHours) || 1, 1); const extendHours = Math.max(Number(body.extendHours) || 1, 1);
@ -122,10 +110,6 @@ eventRoutes.post("/api/events", async (c) => {
const user = await getAuthenticatedUser(c); const user = await getAuthenticatedUser(c);
if (!user) return c.json({ error: "Unauthorized" }, 401); if (!user) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(user, "write:events")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
const body = await c.req.json().catch(() => ({})); const body = await c.req.json().catch(() => ({}));
const { name, appId, role = "viewer", maxSeats = 50, lifespanHours = 3 } = const { name, appId, role = "viewer", maxSeats = 50, lifespanHours = 3 } =
body; body;

View File

@ -2,11 +2,7 @@ import { Hono } from "jsr:@hono/hono@4";
import { sqlWrapper } from "../db.ts"; import { sqlWrapper } from "../db.ts";
import { valkey } from "../valkey.ts"; import { valkey } from "../valkey.ts";
import { auditWrapper } from "../audit.ts"; import { auditWrapper } from "../audit.ts";
import { import { getAuthenticatedUser, getClientIp } from "../auth-session.ts";
getAuthenticatedUser,
getClientIp,
hasScope,
} from "../auth-session.ts";
export const sessionRoutes = new Hono(); export const sessionRoutes = new Hono();
@ -36,13 +32,6 @@ sessionRoutes.post("/api/sessions/delegate", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
// If delegating, the current session must have 'admin' scope or '*' if it's an agent
if (auth.isAgent) {
if (!hasScope(auth, "admin") && !hasScope(auth, "*")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
}
const { const {
label, label,
lifespanHours = 1, lifespanHours = 1,
@ -130,10 +119,6 @@ sessionRoutes.put("/api/sessions/:id/scopes", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(auth, "write:sessions")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
const targetSessionId = c.req.param("id"); const targetSessionId = c.req.param("id");
const { customScopes = [] } = await c.req.json(); const { customScopes = [] } = await c.req.json();
@ -183,10 +168,6 @@ sessionRoutes.post("/api/sessions/:id/extend", async (c) => {
const auth = await getAuthenticatedUser(c); const auth = await getAuthenticatedUser(c);
if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!auth) return c.json({ error: "Unauthorized" }, 401);
if (!hasScope(auth, "write:sessions")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
const targetSessionId = c.req.param("id"); const targetSessionId = c.req.param("id");
const { extendHours = 1 } = await c.req.json(); const { extendHours = 1 } = await c.req.json();
const additionalHours = Math.max(Number(extendHours) || 1, 1); const additionalHours = Math.max(Number(extendHours) || 1, 1);
@ -234,12 +215,6 @@ sessionRoutes.delete("/api/sessions/:id", async (c) => {
const targetSessionId = c.req.param("id"); const targetSessionId = c.req.param("id");
if (targetSessionId !== auth.sessionId) {
if (!hasScope(auth, "write:sessions")) {
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
}
}
const session = await sqlWrapper.sql` const session = await sqlWrapper.sql`
SELECT id FROM sessions WHERE id = ${targetSessionId} AND user_id = ${auth.userId} SELECT id FROM sessions WHERE id = ${targetSessionId} AND user_id = ${auth.userId}
`.then((res: any) => res[0]); `.then((res: any) => res[0]);

View File

@ -9,7 +9,6 @@ import {
getCookieDomain, getCookieDomain,
isGlobalAdmin, isGlobalAdmin,
isSafeRedirectUrl, isSafeRedirectUrl,
isSessionAdmin,
} from "../server/auth-session.ts"; } from "../server/auth-session.ts";
import { auditWrapper } from "../server/audit.ts"; import { auditWrapper } from "../server/audit.ts";
import { LoginPage } from "./components/LoginPage.tsx"; import { LoginPage } from "./components/LoginPage.tsx";
@ -259,9 +258,8 @@ uiApp.get("/admin/users", async (c) => {
} }
const isAdmin = await isGlobalAdmin(auth.userId); const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth); if (!isAdmin) {
if (!isAdmin || !isSessionAdminRole) { return c.redirect("/dashboard");
return c.redirect("/dashboard", 302);
} }
const users = await sql` const users = await sql`
@ -288,9 +286,8 @@ uiApp.get("/admin/apps", async (c) => {
} }
const isAdmin = await isGlobalAdmin(auth.userId); const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth); if (!isAdmin) {
if (!isAdmin || !isSessionAdminRole) { return c.redirect("/dashboard");
return c.redirect("/dashboard", 302);
} }
const apps = await sql` const apps = await sql`
@ -320,9 +317,8 @@ uiApp.get("/admin/roles", async (c) => {
} }
const isAdmin = await isGlobalAdmin(auth.userId); const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth); if (!isAdmin) {
if (!isAdmin || !isSessionAdminRole) { return c.redirect("/dashboard");
return c.redirect("/dashboard", 302);
} }
const roles = await sql` const roles = await sql`
@ -355,9 +351,8 @@ uiApp.get("/admin/invites", async (c) => {
} }
const isAdmin = await isGlobalAdmin(auth.userId); const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth); if (!isAdmin) {
if (!isAdmin || !isSessionAdminRole) { return c.redirect("/dashboard");
return c.redirect("/dashboard", 302);
} }
const invites = await sql` const invites = await sql`
@ -424,9 +419,8 @@ uiApp.get("/admin/users/:id", async (c) => {
} }
const isAdmin = await isGlobalAdmin(auth.userId); const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth); if (!isAdmin) {
if (!isAdmin || !isSessionAdminRole) { return c.redirect("/admin/users");
return c.redirect("/admin/users", 302);
} }
const targetUserId = c.req.param("id"); const targetUserId = c.req.param("id");
@ -496,9 +490,8 @@ uiApp.get("/admin/audit-logs", async (c) => {
} }
const isAdmin = await isGlobalAdmin(auth.userId); const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth); if (!isAdmin) {
if (!isAdmin || !isSessionAdminRole) { return c.redirect("/dashboard");
return c.redirect("/dashboard", 302);
} }
const logs = await sql` const logs = await sql`