import type { AppContext } from "../lib/app-context.js"; import type { Context, Next } from "hono"; import type { Variables } from "../types.js"; import { memberships, roles, rolePermissions } from "@atbb/db"; import { eq, and, or } from "drizzle-orm"; /** * Check if a user has a specific permission. * * @returns true if user has permission, false otherwise * * Returns false (fail closed) if: * - User has no membership * - User has no role assigned (roleUri is null) * - Role not found in database (deleted or invalid) */ async function checkPermission( ctx: AppContext, did: string, permission: string ): Promise { try { // 1. Get user's membership (includes roleUri) const [membership] = await ctx.db .select() .from(memberships) .where(eq(memberships.did, did)) .limit(1); if (!membership || !membership.roleUri) { return false; // No membership or no role assigned = Guest (no permissions) } // 2. Extract rkey from roleUri const roleRkey = membership.roleUri.split("/").pop(); if (!roleRkey) { return false; } // 3. Fetch role definition from roles table const [role] = await ctx.db .select() .from(roles) .where( and( eq(roles.did, ctx.config.forumDid), eq(roles.rkey, roleRkey) ) ) .limit(1); if (!role) { return false; // Role not found = treat as Guest (fail closed) } // 4. Check if user has the permission (wildcard or specific) const [match] = await ctx.db .select() .from(rolePermissions) .where( and( eq(rolePermissions.roleId, role.id), or( eq(rolePermissions.permission, permission), eq(rolePermissions.permission, "*") ) ) ) .limit(1); return !!match; } catch (error) { // Re-throw programming errors (typos, undefined variables, etc.) // These should crash during development, not silently deny access if (error instanceof TypeError || error instanceof ReferenceError || error instanceof SyntaxError) { throw error; } // For expected errors (database connection, network, etc.): // Log and fail closed (deny access) ctx.logger.error("Failed to check permissions", { operation: "checkPermission", did, permission, error: error instanceof Error ? error.message : String(error), }); return false; } } /** * Get a user's role definition. * * @returns Role object or null if user has no role (fail closed on error) */ async function getUserRole( ctx: AppContext, did: string ): Promise<{ id: bigint; name: string; priority: number } | null> { try { const [membership] = await ctx.db .select() .from(memberships) .where(eq(memberships.did, did)) .limit(1); if (!membership || !membership.roleUri) { return null; } const roleRkey = membership.roleUri.split("/").pop(); if (!roleRkey) { return null; } const [role] = await ctx.db .select({ id: roles.id, name: roles.name, priority: roles.priority, }) .from(roles) .where( and( eq(roles.did, ctx.config.forumDid), eq(roles.rkey, roleRkey) ) ) .limit(1); return role || null; } catch (error) { // Fail closed: return null on any error to deny access ctx.logger.error("Failed to query user role", { did, error: error instanceof Error ? error.message : String(error), }); return null; } } /** * Check if a user has a minimum role level. * * @param minRole - Minimum required role name * @returns true if user's role priority <= required priority (higher authority) */ async function checkMinRole( ctx: AppContext, did: string, minRole: string ): Promise { const rolePriorities: Record = { owner: 0, admin: 10, moderator: 20, member: 30, }; const userRole = await getUserRole(ctx, did); if (!userRole) { return false; // No role = Guest (fails all role checks) } const userPriority = userRole.priority; const requiredPriority = rolePriorities[minRole]; // Lower priority value = higher authority return userPriority <= requiredPriority; } /** * Check if an actor can perform moderation actions on a target user. * * Priority hierarchy enforcement: * - Users can always act on themselves (self-action bypass) * - Can only act on users with strictly lower authority (higher priority value) * - Cannot act on users with equal or higher authority * * @returns true if actor can act on target, false otherwise */ export async function canActOnUser( ctx: AppContext, actorDid: string, targetDid: string ): Promise { // Users can always act on themselves if (actorDid === targetDid) { return true; } const actorRole = await getUserRole(ctx, actorDid); const targetRole = await getUserRole(ctx, targetDid); // If actor has no role, they can't act on anyone else if (!actorRole) { return false; } // If target has no role (Guest), anyone with a role can act on them if (!targetRole) { return true; } // Lower priority = higher authority // Can only act on users with strictly higher priority value (lower authority) return actorRole.priority < targetRole.priority; } /** * Require specific permission middleware. * * Validates that the authenticated user has the required permission token. * Returns 401 if not authenticated, 403 if authenticated but lacks permission. */ export function requirePermission( ctx: AppContext, permission: string ) { return async (c: Context<{ Variables: Variables }>, next: Next) => { const user = c.get("user"); if (!user) { return c.json({ error: "Authentication required" }, 401); } const hasPermission = await checkPermission(ctx, user.did, permission); if (!hasPermission) { return c.json({ error: "Insufficient permissions", required: permission }, 403); } await next(); }; } /** * Require at least one of a list of permissions (OR logic). * * Iterates the permissions list in order, calling checkPermission for each. * Short-circuits and calls next() on the first match. * Returns 401 if not authenticated, 403 if none of the permissions match. */ export function requireAnyPermission( ctx: AppContext, permissions: string[] ) { return async (c: Context<{ Variables: Variables }>, next: Next) => { const user = c.get("user"); if (!user) { return c.json({ error: "Authentication required" }, 401); } for (const permission of permissions) { const hasPermission = await checkPermission(ctx, user.did, permission); if (hasPermission) { await next(); return; } } return c.json({ error: "Insufficient permissions" }, 403); }; } /** * Require minimum role middleware. * * Validates that the authenticated user has a role with sufficient priority. * Returns 401 if not authenticated, 403 if authenticated but insufficient role. */ export function requireRole( ctx: AppContext, minRole: "owner" | "admin" | "moderator" | "member" ) { return async (c: Context<{ Variables: Variables }>, next: Next) => { const user = c.get("user"); if (!user) { return c.json({ error: "Authentication required" }, 401); } const hasRole = await checkMinRole(ctx, user.did, minRole); if (!hasRole) { return c.json({ error: "Insufficient role", required: minRole }, 403); } await next(); }; } // Export helpers for testing export { checkPermission, getUserRole, checkMinRole };