feat(perm): load capabilities + bot access onto req.user; expose via /me

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-05-30 13:21:24 +08:00
1 parent 7a8666efbb
commit 696b224f8d
8 files changed
+42 -12

No files matched your search

+3 -1
View File
@@ -6,6 +6,7 @@ import { createDatabase, type BotDatabase } from "../../data/database.js";
import { createUserStore } from "../../data/users.js"; import { createUserStore } from "../../data/users.js";
import { createSessionStore } from "../../data/sessions.js"; import { createSessionStore } from "../../data/sessions.js";
import { createAuditStore } from "../../data/audit.js"; import { createAuditStore } from "../../data/audit.js";
import { createPermissionStore } from "../../data/permissions.js";
import { createRequireAuth } from "../middleware/requireAuth.js"; import { createRequireAuth } from "../middleware/requireAuth.js";
import { createAuditRouter } from "./audit.js"; import { createAuditRouter } from "./audit.js";
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
@@ -20,6 +21,7 @@ describe("audit router", () => {
const users = createUserStore(botDb.db); const users = createUserStore(botDb.db);
const sessions = createSessionStore(botDb.db); const sessions = createSessionStore(botDb.db);
const audit = createAuditStore(botDb.db); const audit = createAuditStore(botDb.db);
const permissions = createPermissionStore(botDb.db);
const alice = await users.createUser("alice", "pw-alice", "admin"); const alice = await users.createUser("alice", "pw-alice", "admin");
cookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(alice.id).token}`; cookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(alice.id).token}`;
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
@@ -32,7 +34,7 @@ describe("audit router", () => {
app = express(); app = express();
app.use(express.json()); app.use(express.json());
app.use(cookieParser()); app.use(cookieParser());
app.use("/api", createRequireAuth(sessions)); app.use("/api", createRequireAuth(sessions, permissions));
app.use("/api/audit", createAuditRouter(audit)); app.use("/api/audit", createAuditRouter(audit));
}); });
+3 -1
View File
@@ -7,6 +7,7 @@ import { createDatabase, type BotDatabase } from "../../data/database.js";
import { createUserStore, type UserStore } from "../../data/users.js"; import { createUserStore, type UserStore } from "../../data/users.js";
import { createSessionStore, type SessionStore } from "../../data/sessions.js"; import { createSessionStore, type SessionStore } from "../../data/sessions.js";
import { createAuditStore } from "../../data/audit.js"; import { createAuditStore } from "../../data/audit.js";
import { createPermissionStore } from "../../data/permissions.js";
import { createSessionRouter } from "./session.js"; import { createSessionRouter } from "./session.js";
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
@@ -15,7 +16,8 @@ function makeApp(botDb: BotDatabase, users: UserStore, sessions: SessionStore) {
app.use(express.json()); app.use(express.json());
app.use(cookieParser()); app.use(cookieParser());
const audit = createAuditStore(botDb.db); const audit = createAuditStore(botDb.db);
app.use("/api/session", createSessionRouter(users, sessions, audit, pino({ level: "silent" }))); const permissions = createPermissionStore(botDb.db);
app.use("/api/session", createSessionRouter(users, sessions, audit, pino({ level: "silent" }), permissions));
return app; return app;
} }
+12 -2
View File
@@ -4,6 +4,7 @@ import type { Logger } from "../../logger.js";
import type { UserStore } from "../../data/users.js"; import type { UserStore } from "../../data/users.js";
import type { SessionStore } from "../../data/sessions.js"; import type { SessionStore } from "../../data/sessions.js";
import type { AuditStore } from "../../data/audit.js"; import type { AuditStore } from "../../data/audit.js";
import { resolvePermissionContext, type PermissionStore } from "../../data/permissions.js";
import { SESSION_TTL_MS } from "../../data/sessions.js"; import { SESSION_TTL_MS } from "../../data/sessions.js";
import { SESSION_COOKIE_NAME, validateSessionFromHeaders, extractSessionToken } from "../auth/validateSession.js"; import { SESSION_COOKIE_NAME, validateSessionFromHeaders, extractSessionToken } from "../auth/validateSession.js";
@@ -49,7 +50,8 @@ export function createSessionRouter(
users: UserStore, users: UserStore,
sessions: SessionStore, sessions: SessionStore,
audit: AuditStore, audit: AuditStore,
logger: Logger logger: Logger,
permissions: PermissionStore
): Router { ): Router {
const router = Router(); const router = Router();
@@ -133,7 +135,15 @@ export function createSessionRouter(
}); });
router.get("/me", requireAuthInline, (req, res) => { router.get("/me", requireAuthInline, (req, res) => {
res.json(req.user); const user = req.user!;
const ctx = resolvePermissionContext(user.role, user.id, permissions);
res.json({
id: user.id,
username: user.username,
role: user.role,
capabilities: [...ctx.capabilities],
bots: ctx.bots === "all" ? "all" : [...ctx.bots],
});
}); });
router.post("/change-password", requireAuthInline, async (req, res) => { router.post("/change-password", requireAuthInline, async (req, res) => {
+4 -2
View File
@@ -7,6 +7,7 @@ import { createDatabase, type BotDatabase } from "../../data/database.js";
import { createUserStore, type UserStore } from "../../data/users.js"; import { createUserStore, type UserStore } from "../../data/users.js";
import { createSessionStore, type SessionStore } from "../../data/sessions.js"; import { createSessionStore, type SessionStore } from "../../data/sessions.js";
import { createAuditStore } from "../../data/audit.js"; import { createAuditStore } from "../../data/audit.js";
import { createPermissionStore } from "../../data/permissions.js";
import { createRequireAuth } from "../middleware/requireAuth.js"; import { createRequireAuth } from "../middleware/requireAuth.js";
import { createUsersRouter } from "./users.js"; import { createUsersRouter } from "./users.js";
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
@@ -15,7 +16,8 @@ function makeApp(botDb: BotDatabase, users: UserStore, sessions: SessionStore) {
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
app.use(cookieParser()); app.use(cookieParser());
const requireAuth = createRequireAuth(sessions); const permissions = createPermissionStore(botDb.db);
const requireAuth = createRequireAuth(sessions, permissions);
const audit = createAuditStore(botDb.db); const audit = createAuditStore(botDb.db);
app.use("/api", requireAuth); app.use("/api", requireAuth);
app.use("/api/users", createUsersRouter(users, sessions, audit, pino({ level: "silent" }))); app.use("/api/users", createUsersRouter(users, sessions, audit, pino({ level: "silent" })));
@@ -148,7 +150,7 @@ describe("users router", () => {
const localApp = express(); const localApp = express();
localApp.use(express.json()); localApp.use(express.json());
localApp.use(cookieParser()); localApp.use(cookieParser());
localApp.use("/api", createRequireAuth(sessions)); localApp.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db)));
localApp.use( localApp.use(
"/api/users", "/api/users",
createUsersRouter(users, sessions, brokenAudit, pino({ level: "silent" })) createUsersRouter(users, sessions, brokenAudit, pino({ level: "silent" }))
+3 -1
View File
@@ -5,6 +5,7 @@ import request from "supertest";
import { createDatabase, type BotDatabase } from "../../data/database.js"; import { createDatabase, type BotDatabase } from "../../data/database.js";
import { createUserStore } from "../../data/users.js"; import { createUserStore } from "../../data/users.js";
import { createSessionStore } from "../../data/sessions.js"; import { createSessionStore } from "../../data/sessions.js";
import { createPermissionStore } from "../../data/permissions.js";
import { createRequireAuth } from "./requireAuth.js"; import { createRequireAuth } from "./requireAuth.js";
import { requireAdmin } from "./requireAdmin.js"; import { requireAdmin } from "./requireAdmin.js";
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
@@ -19,13 +20,14 @@ describe("requireAdmin middleware", () => {
botDb = createDatabase(":memory:"); botDb = createDatabase(":memory:");
const users = createUserStore(botDb.db); const users = createUserStore(botDb.db);
const sessions = createSessionStore(botDb.db); const sessions = createSessionStore(botDb.db);
const permissions = createPermissionStore(botDb.db);
const admin = await users.createUser("admin", "pw-admin-pw", "admin"); const admin = await users.createUser("admin", "pw-admin-pw", "admin");
const member = await users.createUser("member", "pw-member-pw", "member"); const member = await users.createUser("member", "pw-member-pw", "member");
adminCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(admin.id).token}`; adminCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(admin.id).token}`;
memberCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(member.id).token}`; memberCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(member.id).token}`;
app = express(); app = express();
app.use(cookieParser()); app.use(cookieParser());
app.use(createRequireAuth(sessions)); app.use(createRequireAuth(sessions, permissions));
app.use(requireAdmin); app.use(requireAdmin);
app.get("/admin-only", (_req, res) => res.json({ ok: true })); app.get("/admin-only", (_req, res) => res.json({ ok: true }));
}); });
+3 -1
View File
@@ -5,6 +5,7 @@ import request from "supertest";
import { createDatabase, type BotDatabase } from "../../data/database.js"; import { createDatabase, type BotDatabase } from "../../data/database.js";
import { createUserStore } from "../../data/users.js"; import { createUserStore } from "../../data/users.js";
import { createSessionStore } from "../../data/sessions.js"; import { createSessionStore } from "../../data/sessions.js";
import { createPermissionStore } from "../../data/permissions.js";
import { createRequireAuth } from "./requireAuth.js"; import { createRequireAuth } from "./requireAuth.js";
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
@@ -17,12 +18,13 @@ describe("requireAuth middleware", () => {
botDb = createDatabase(":memory:"); botDb = createDatabase(":memory:");
const users = createUserStore(botDb.db); const users = createUserStore(botDb.db);
const sessions = createSessionStore(botDb.db); const sessions = createSessionStore(botDb.db);
const permissions = createPermissionStore(botDb.db);
const u = await users.createUser("alice", "pw-alice", "admin"); const u = await users.createUser("alice", "pw-alice", "admin");
validToken = sessions.createSession(u.id).token; validToken = sessions.createSession(u.id).token;
app = express(); app = express();
app.use(cookieParser()); app.use(cookieParser());
app.use(createRequireAuth(sessions)); app.use(createRequireAuth(sessions, permissions));
app.get("/protected", (req, res) => { app.get("/protected", (req, res) => {
res.json({ ok: true, user: (req as any).user }); res.json({ ok: true, user: (req as any).user });
}); });
+10 -2
View File
@@ -1,6 +1,7 @@
import type { Request, Response, NextFunction, RequestHandler } from "express"; import type { Request, Response, NextFunction, RequestHandler } from "express";
import type { SessionStore } from "../../data/sessions.js"; import type { SessionStore } from "../../data/sessions.js";
import { SESSION_TTL_MS } from "../../data/sessions.js"; import { SESSION_TTL_MS } from "../../data/sessions.js";
import { resolvePermissionContext, type PermissionStore } from "../../data/permissions.js";
import { import {
validateSessionFromHeaders, validateSessionFromHeaders,
extractSessionToken, extractSessionToken,
@@ -19,7 +20,7 @@ declare module "express-serve-static-core" {
} }
} }
export function createRequireAuth(sessions: SessionStore): RequestHandler { export function createRequireAuth(sessions: SessionStore, permissions: PermissionStore): RequestHandler {
return function requireAuth(req: Request, res: Response, next: NextFunction) { return function requireAuth(req: Request, res: Response, next: NextFunction) {
const result = validateSessionFromHeaders(req.headers.cookie, sessions); const result = validateSessionFromHeaders(req.headers.cookie, sessions);
if (!result) { if (!result) {
@@ -27,7 +28,14 @@ export function createRequireAuth(sessions: SessionStore): RequestHandler {
res.status(401).json({ error: "unauthenticated" }); res.status(401).json({ error: "unauthenticated" });
return; return;
} }
req.user = { id: result.userId, username: result.username, role: result.role }; const ctx = resolvePermissionContext(result.role, result.userId, permissions);
req.user = {
id: result.userId,
username: result.username,
role: result.role,
capabilities: ctx.capabilities,
bots: ctx.bots,
};
const token = extractSessionToken(req.headers.cookie); const token = extractSessionToken(req.headers.cookie);
if (token) { if (token) {
res.cookie(SESSION_COOKIE_NAME, token, { res.cookie(SESSION_COOKIE_NAME, token, {
+4 -2
View File
@@ -21,6 +21,7 @@ import { createAuditRouter } from "./api/audit.js";
import { setupWebSocket } from "./websocket.js"; import { setupWebSocket } from "./websocket.js";
import { createUserStore } from "../data/users.js"; import { createUserStore } from "../data/users.js";
import { createSessionStore } from "../data/sessions.js"; import { createSessionStore } from "../data/sessions.js";
import { createPermissionStore } from "../data/permissions.js";
import { createRequireAuth } from "./middleware/requireAuth.js"; import { createRequireAuth } from "./middleware/requireAuth.js";
import { requireAdmin } from "./middleware/requireAdmin.js"; import { requireAdmin } from "./middleware/requireAdmin.js";
import { csrfOriginCheck } from "./middleware/csrf.js"; import { csrfOriginCheck } from "./middleware/csrf.js";
@@ -73,6 +74,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
const users = createUserStore(options.database.db); const users = createUserStore(options.database.db);
const sessions = createSessionStore(options.database.db); const sessions = createSessionStore(options.database.db);
const audit = createAuditStore(options.database.db); const audit = createAuditStore(options.database.db);
const permissions = createPermissionStore(options.database.db);
// ─── Public routes (no auth, no CSRF) ─────────────────────────────────── // ─── Public routes (no auth, no CSRF) ───────────────────────────────────
app.get("/api/health", (_req, res) => { app.get("/api/health", (_req, res) => {
@@ -92,10 +94,10 @@ export function createWebServer(options: WebServerOptions): WebServer {
app.use("/api/session/login", loginLimit); app.use("/api/session/login", loginLimit);
app.use("/api/session/setup", setupLimit); app.use("/api/session/setup", setupLimit);
app.use("/api/session", createSessionRouter(users, sessions, audit, logger)); app.use("/api/session", createSessionRouter(users, sessions, audit, logger, permissions));
// ─── Gates for everything else under /api ─────────────────────────────── // ─── Gates for everything else under /api ───────────────────────────────
const requireAuth = createRequireAuth(sessions); const requireAuth = createRequireAuth(sessions, permissions);
app.use("/api", csrfOriginCheck); app.use("/api", csrfOriginCheck);
app.use("/api", requireAuth); app.use("/api", requireAuth);