From d235d747ca97f44b1e9dd8156035e669a6bd168a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 14 Feb 2026 12:47:03 -0800 Subject: [PATCH] more permissions stuff --- .../app/api/credentials/[id]/members/route.ts | 17 ++++++++-- apps/sim/app/api/credentials/draft/route.ts | 33 ++++++++++++++++++- .../[id]/members/[memberId]/route.ts | 26 +++++++++++++-- .../app/api/workspaces/members/[id]/route.ts | 26 +++++++++++++-- 4 files changed, 94 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/api/credentials/[id]/members/route.ts b/apps/sim/app/api/credentials/[id]/members/route.ts index 41cf30a69..b76827a07 100644 --- a/apps/sim/app/api/credentials/[id]/members/route.ts +++ b/apps/sim/app/api/credentials/[id]/members/route.ts @@ -13,7 +13,18 @@ interface RouteContext { params: Promise<{ id: string }> } -async function requireAdminMembership(credentialId: string, userId: string) { +async function requireWorkspaceAdminMembership(credentialId: string, userId: string) { + const [cred] = await db + .select({ id: credential.id, workspaceId: credential.workspaceId }) + .from(credential) + .where(eq(credential.id, credentialId)) + .limit(1) + + if (!cred) return null + + const perm = await getUserEntityPermissions(userId, 'workspace', cred.workspaceId) + if (perm === null) return null + const [membership] = await db .select({ role: credentialMember.role, status: credentialMember.status }) .from(credentialMember) @@ -91,7 +102,7 @@ export async function POST(request: NextRequest, context: RouteContext) { const { id: credentialId } = await context.params - const admin = await requireAdminMembership(credentialId, session.user.id) + const admin = await requireWorkspaceAdminMembership(credentialId, session.user.id) if (!admin) { return NextResponse.json({ error: 'Admin access required' }, { status: 403 }) } @@ -153,7 +164,7 @@ export async function DELETE(request: NextRequest, context: RouteContext) { return NextResponse.json({ error: 'userId query parameter required' }, { status: 400 }) } - const admin = await requireAdminMembership(credentialId, session.user.id) + const admin = await requireWorkspaceAdminMembership(credentialId, session.user.id) if (!admin) { return NextResponse.json({ error: 'Admin access required' }, { status: 403 }) } diff --git a/apps/sim/app/api/credentials/draft/route.ts b/apps/sim/app/api/credentials/draft/route.ts index 29addeea2..ac700f088 100644 --- a/apps/sim/app/api/credentials/draft/route.ts +++ b/apps/sim/app/api/credentials/draft/route.ts @@ -1,10 +1,11 @@ import { db } from '@sim/db' -import { pendingCredentialDraft } from '@sim/db/schema' +import { credential, credentialMember, pendingCredentialDraft } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, lt } from 'drizzle-orm' import { NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CredentialDraftAPI') @@ -33,6 +34,36 @@ export async function POST(request: Request) { const { workspaceId, providerId, displayName, description, credentialId } = parsed.data const userId = session.user.id + + const workspaceAccess = await checkWorkspaceAccess(workspaceId, userId) + if (!workspaceAccess.canWrite) { + return NextResponse.json({ error: 'Write permission required' }, { status: 403 }) + } + + if (credentialId) { + const [membership] = await db + .select({ role: credentialMember.role, status: credentialMember.status }) + .from(credentialMember) + .innerJoin(credential, eq(credential.id, credentialMember.credentialId)) + .where( + and( + eq(credentialMember.credentialId, credentialId), + eq(credentialMember.userId, userId), + eq(credentialMember.status, 'active'), + eq(credentialMember.role, 'admin'), + eq(credential.workspaceId, workspaceId) + ) + ) + .limit(1) + + if (!membership) { + return NextResponse.json( + { error: 'Admin access required on the target credential' }, + { status: 403 } + ) + } + } + const now = new Date() await db diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts index 49092d86d..73c47d890 100644 --- a/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts @@ -22,9 +22,9 @@ */ import { db } from '@sim/db' -import { permissions, user, workspace } from '@sim/db/schema' +import { credential, credentialMember, permissions, user, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray } from 'drizzle-orm' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { badRequestResponse, @@ -215,6 +215,28 @@ export const DELETE = withAdminAuthParams(async (_, context) => { await db.delete(permissions).where(eq(permissions.id, memberId)) + // Revoke credential memberships for all credentials in this workspace + const workspaceCredentialIds = await db + .select({ id: credential.id }) + .from(credential) + .where(eq(credential.workspaceId, workspaceId)) + + if (workspaceCredentialIds.length > 0) { + await db + .update(credentialMember) + .set({ status: 'revoked', updatedAt: new Date() }) + .where( + and( + eq(credentialMember.userId, existingMember.userId), + eq(credentialMember.status, 'active'), + inArray( + credentialMember.credentialId, + workspaceCredentialIds.map((c) => c.id) + ) + ) + ) + } + logger.info(`Admin API: Removed member ${memberId} from workspace ${workspaceId}`, { userId: existingMember.userId, }) diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts index ec990da24..ae677e27b 100644 --- a/apps/sim/app/api/workspaces/members/[id]/route.ts +++ b/apps/sim/app/api/workspaces/members/[id]/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' -import { permissions, workspace } from '@sim/db/schema' +import { credential, credentialMember, permissions, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -101,6 +101,28 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i ) ) + // Revoke credential memberships for all credentials in this workspace + const workspaceCredentialIds = await db + .select({ id: credential.id }) + .from(credential) + .where(eq(credential.workspaceId, workspaceId)) + + if (workspaceCredentialIds.length > 0) { + await db + .update(credentialMember) + .set({ status: 'revoked', updatedAt: new Date() }) + .where( + and( + eq(credentialMember.userId, userId), + eq(credentialMember.status, 'active'), + inArray( + credentialMember.credentialId, + workspaceCredentialIds.map((c) => c.id) + ) + ) + ) + } + return NextResponse.json({ success: true }) } catch (error) { logger.error('Error removing workspace member:', error)