Compare commits
2 Commits
563723c654
...
aa6b731fcf
| Author | SHA1 | Date | |
|---|---|---|---|
| aa6b731fcf | |||
| 6a5a769203 |
854
scratch/jules_scope_guards.patch
Normal file
854
scratch/jules_scope_guards.patch
Normal file
@ -0,0 +1,854 @@
|
|||||||
|
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`
|
||||||
@ -405,6 +405,75 @@ 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) {
|
||||||
|
|||||||
@ -1330,3 +1330,247 @@ 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();
|
||||||
|
});
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import {
|
|||||||
extractAllSessionIds,
|
extractAllSessionIds,
|
||||||
getAuthenticatedUser,
|
getAuthenticatedUser,
|
||||||
isGlobalAdmin,
|
isGlobalAdmin,
|
||||||
|
requireAdmin,
|
||||||
|
requirePrimarySession,
|
||||||
} from "./auth-session.ts";
|
} from "./auth-session.ts";
|
||||||
import {
|
import {
|
||||||
decodeBase64Url,
|
decodeBase64Url,
|
||||||
@ -175,6 +177,7 @@ 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) {
|
||||||
@ -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)
|
// Provisioning & Registration (Use Cases 1, 2, 3)
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
@ -200,10 +206,6 @@ 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,
|
||||||
@ -1048,10 +1050,6 @@ 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
|
||||||
@ -1067,10 +1065,6 @@ 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
|
||||||
@ -1084,10 +1078,6 @@ 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();
|
||||||
|
|
||||||
@ -1114,10 +1104,6 @@ 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();
|
||||||
|
|
||||||
@ -1143,9 +1129,6 @@ 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,
|
||||||
@ -1161,9 +1144,6 @@ 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,
|
||||||
@ -1207,9 +1187,6 @@ 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 {
|
||||||
@ -1253,9 +1230,6 @@ 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
|
||||||
@ -1281,9 +1255,6 @@ 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;
|
||||||
@ -1312,9 +1283,6 @@ 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 (
|
||||||
@ -1373,9 +1341,6 @@ 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();
|
||||||
@ -1410,9 +1375,6 @@ 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
|
||||||
@ -1445,9 +1407,6 @@ 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,
|
||||||
@ -1464,9 +1423,6 @@ 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`
|
||||||
@ -1483,9 +1439,6 @@ 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
|
||||||
@ -1511,9 +1464,6 @@ 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`
|
||||||
@ -1529,9 +1479,6 @@ 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();
|
||||||
@ -1572,9 +1519,6 @@ 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();
|
||||||
|
|
||||||
@ -1605,9 +1549,6 @@ 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 });
|
||||||
@ -1616,9 +1557,6 @@ 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;
|
||||||
@ -1649,9 +1587,6 @@ 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`
|
||||||
@ -1675,9 +1610,6 @@ 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") {
|
||||||
@ -1721,9 +1653,6 @@ 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");
|
||||||
|
|
||||||
@ -1755,9 +1684,6 @@ 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}`
|
||||||
@ -1773,9 +1699,6 @@ 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`
|
||||||
@ -1800,9 +1723,6 @@ 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`;
|
||||||
@ -1824,9 +1744,6 @@ 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`
|
||||||
@ -1847,9 +1764,6 @@ 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}`
|
||||||
|
|||||||
@ -3,7 +3,11 @@ 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 { getAuthenticatedUser, getCookieDomain } from "../auth-session.ts";
|
import {
|
||||||
|
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";
|
||||||
|
|
||||||
@ -17,6 +21,10 @@ 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 {
|
||||||
@ -57,6 +65,10 @@ 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);
|
||||||
@ -110,6 +122,10 @@ 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;
|
||||||
|
|||||||
@ -2,7 +2,11 @@ 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 { getAuthenticatedUser, getClientIp } from "../auth-session.ts";
|
import {
|
||||||
|
getAuthenticatedUser,
|
||||||
|
getClientIp,
|
||||||
|
hasScope,
|
||||||
|
} from "../auth-session.ts";
|
||||||
|
|
||||||
export const sessionRoutes = new Hono();
|
export const sessionRoutes = new Hono();
|
||||||
|
|
||||||
@ -32,6 +36,13 @@ 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,
|
||||||
@ -119,6 +130,10 @@ 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();
|
||||||
|
|
||||||
@ -168,6 +183,10 @@ 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);
|
||||||
@ -215,6 +234,12 @@ 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]);
|
||||||
|
|||||||
31
ui/mod.ts
31
ui/mod.ts
@ -9,6 +9,7 @@ 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";
|
||||||
@ -258,8 +259,9 @@ uiApp.get("/admin/users", async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = await isGlobalAdmin(auth.userId);
|
const isAdmin = await isGlobalAdmin(auth.userId);
|
||||||
if (!isAdmin) {
|
const isSessionAdminRole = await isSessionAdmin(auth);
|
||||||
return c.redirect("/dashboard");
|
if (!isAdmin || !isSessionAdminRole) {
|
||||||
|
return c.redirect("/dashboard", 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await sql`
|
const users = await sql`
|
||||||
@ -286,8 +288,9 @@ uiApp.get("/admin/apps", async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = await isGlobalAdmin(auth.userId);
|
const isAdmin = await isGlobalAdmin(auth.userId);
|
||||||
if (!isAdmin) {
|
const isSessionAdminRole = await isSessionAdmin(auth);
|
||||||
return c.redirect("/dashboard");
|
if (!isAdmin || !isSessionAdminRole) {
|
||||||
|
return c.redirect("/dashboard", 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
const apps = await sql`
|
const apps = await sql`
|
||||||
@ -317,8 +320,9 @@ uiApp.get("/admin/roles", async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = await isGlobalAdmin(auth.userId);
|
const isAdmin = await isGlobalAdmin(auth.userId);
|
||||||
if (!isAdmin) {
|
const isSessionAdminRole = await isSessionAdmin(auth);
|
||||||
return c.redirect("/dashboard");
|
if (!isAdmin || !isSessionAdminRole) {
|
||||||
|
return c.redirect("/dashboard", 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roles = await sql`
|
const roles = await sql`
|
||||||
@ -351,8 +355,9 @@ uiApp.get("/admin/invites", async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = await isGlobalAdmin(auth.userId);
|
const isAdmin = await isGlobalAdmin(auth.userId);
|
||||||
if (!isAdmin) {
|
const isSessionAdminRole = await isSessionAdmin(auth);
|
||||||
return c.redirect("/dashboard");
|
if (!isAdmin || !isSessionAdminRole) {
|
||||||
|
return c.redirect("/dashboard", 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
const invites = await sql`
|
const invites = await sql`
|
||||||
@ -419,8 +424,9 @@ uiApp.get("/admin/users/:id", async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = await isGlobalAdmin(auth.userId);
|
const isAdmin = await isGlobalAdmin(auth.userId);
|
||||||
if (!isAdmin) {
|
const isSessionAdminRole = await isSessionAdmin(auth);
|
||||||
return c.redirect("/admin/users");
|
if (!isAdmin || !isSessionAdminRole) {
|
||||||
|
return c.redirect("/admin/users", 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetUserId = c.req.param("id");
|
const targetUserId = c.req.param("id");
|
||||||
@ -490,8 +496,9 @@ uiApp.get("/admin/audit-logs", async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = await isGlobalAdmin(auth.userId);
|
const isAdmin = await isGlobalAdmin(auth.userId);
|
||||||
if (!isAdmin) {
|
const isSessionAdminRole = await isSessionAdmin(auth);
|
||||||
return c.redirect("/dashboard");
|
if (!isAdmin || !isSessionAdminRole) {
|
||||||
|
return c.redirect("/dashboard", 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = await sql`
|
const logs = await sql`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user