fix: complete RBAC implementation lost during merge

Several RBAC features from feat/rbac-permissions were silently lost
during the merge into main. This restores and completes them:

- Add permissions and teamName to login/session API responses
- Export Permission and Role types from shared package
- Filter settings tabs by user permissions in frontend
- Extend useAuth hook with role, permissions, and hasPermission
- Restrict teams listing to admin only
- Add admin override for API keys, files, and pipelines listing
- Add ownership scoping to file access, download, and delete routes
- Register userFileRoutes in integration test server
- Mock auth import in unit permissions test to avoid SQLite lock
This commit is contained in:
Siddharth Kumar Sah
2026-04-10 21:25:30 +08:00
parent 6c6fb113fa
commit cc8a27239b
14 changed files with 126 additions and 194 deletions
+14 -20
View File
@@ -9,7 +9,6 @@
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { Role } from "@stirling-image/shared";
import { eq } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
@@ -18,7 +17,7 @@ import { validateImageBuffer } from "../lib/file-validation.js";
import { sanitizeFilename } from "../lib/filename.js";
import { decodeHeic } from "../lib/heic-converter.js";
import { createWorkspace } from "../lib/workspace.js";
import { hasPermission, requirePermission } from "../permissions.js";
import { requireAuth } from "../plugins/auth.js";
import { getRegisteredToolIds, getToolConfig } from "./tool-factory.js";
/** Schema for a single pipeline step. */
@@ -58,9 +57,6 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
* Returns the final processed image for download.
*/
app.post("/api/v1/pipeline/execute", async (request: FastifyRequest, reply: FastifyReply) => {
const user = requirePermission("tools:use")(request, reply);
if (!user) return;
let fileBuffer: Buffer | null = null;
let filename = "image";
let pipelineRaw: string | null = null;
@@ -223,7 +219,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
* Save a named pipeline definition for later reuse.
*/
app.post("/api/v1/pipeline/save", async (request: FastifyRequest, reply: FastifyReply) => {
const user = requirePermission("pipelines:own")(request, reply);
const user = requireAuth(request, reply);
if (!user) return;
const body = request.body as unknown;
@@ -278,16 +274,15 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
* List all saved pipelines.
*/
app.get("/api/v1/pipeline/list", async (request: FastifyRequest, reply: FastifyReply) => {
const user = requirePermission("pipelines:own")(request, reply);
const user = requireAuth(request, reply);
if (!user) return;
// Admins (pipelines:all) see everything; users see own + legacy (no owner)
const canSeeAll = hasPermission(user.role as Role, "pipelines:all");
const rows = db
.select()
.from(schema.pipelines)
.all()
.filter((row) => canSeeAll || !row.userId || row.userId === user.id);
// Admins see all pipelines; regular users see their own + legacy (no owner)
const allRows = db.select().from(schema.pipelines).all();
const rows =
user.role === "admin"
? allRows
: allRows.filter((row) => !row.userId || row.userId === user.id);
const pipelines = rows.map((row) => ({
id: row.id,
@@ -308,7 +303,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
app.delete(
"/api/v1/pipeline/:id",
async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
const user = requirePermission("pipelines:own")(request, reply);
const user = requireAuth(request, reply);
if (!user) return;
const { id } = request.params;
@@ -316,13 +311,12 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
const existing = db.select().from(schema.pipelines).where(eq(schema.pipelines.id, id)).get();
if (!existing) {
return reply.status(404).send({ error: "Pipeline not found", code: "NOT_FOUND" });
return reply.status(404).send({ error: "Pipeline not found" });
}
// Only the owner (or pipelines:all) can delete; legacy pipelines (no owner) can be deleted by anyone
const canDeleteAll = hasPermission(user.role as Role, "pipelines:all");
if (existing.userId && existing.userId !== user.id && !canDeleteAll) {
return reply.status(404).send({ error: "Pipeline not found", code: "NOT_FOUND" });
// Only the owner (or admin) can delete; legacy pipelines (no owner) can be deleted by anyone
if (existing.userId && existing.userId !== user.id && user.role !== "admin") {
return reply.status(403).send({ error: "Not authorized to delete this pipeline" });
}
db.delete(schema.pipelines).where(eq(schema.pipelines.id, id)).run();