Skip to content

Commit df7d486

Browse files
committed
address comments
1 parent 93e9c98 commit df7d486

16 files changed

Lines changed: 216 additions & 103 deletions

File tree

apps/sim/app/api/workspaces/[id]/permissions/route.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment'
1212
import { applyWorkspaceAutoAddGroup } from '@/lib/permission-groups/auto-add'
1313
import { captureServerEvent } from '@/lib/posthog/server'
1414
import {
15+
checkWorkspaceAccess,
1516
getUsersWithPermissions,
1617
hasWorkspaceAdminAccess,
1718
} from '@/lib/workspaces/permissions/utils'
@@ -46,19 +47,21 @@ export const GET = withRouteHandler(
4647
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
4748
}
4849

49-
const userPermission = await db
50-
.select()
51-
.from(permissions)
52-
.where(
53-
and(
54-
eq(permissions.entityId, workspaceId),
55-
eq(permissions.entityType, 'workspace'),
56-
eq(permissions.userId, session.user.id)
50+
const isAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId)
51+
52+
let hasAccess = isAdmin
53+
if (!hasAccess) {
54+
const access = await checkWorkspaceAccess(workspaceId, session.user.id)
55+
if (!access.exists) {
56+
return NextResponse.json(
57+
{ error: 'Workspace not found or access denied' },
58+
{ status: 404 }
5759
)
58-
)
59-
.limit(1)
60+
}
61+
hasAccess = access.hasAccess
62+
}
6063

61-
if (userPermission.length === 0) {
64+
if (!hasAccess) {
6265
return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 })
6366
}
6467

@@ -67,6 +70,10 @@ export const GET = withRouteHandler(
6770
return NextResponse.json({
6871
users: result,
6972
total: result.length,
73+
viewer: {
74+
userId: session.user.id,
75+
isAdmin,
76+
},
7077
})
7178
} catch (error) {
7279
logger.error('Error fetching workspace permissions:', error)

apps/sim/app/api/workspaces/[id]/route.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ export const PATCH = withRouteHandler(
184184
)
185185
}
186186

187+
if (existingWorkspace.workspaceMode === 'personal') {
188+
return NextResponse.json(
189+
{
190+
error:
191+
'Personal workspaces are always billed to their owner and cannot change billed account.',
192+
},
193+
{ status: 400 }
194+
)
195+
}
196+
187197
const candidateId = billedAccountUserId
188198

189199
const isOwner = candidateId === existingWorkspace.ownerId

apps/sim/app/api/workspaces/[workspaceId]/permission-groups/[id]/members/bulk/route.ts

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server'
77
import { z } from 'zod'
88
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
99
import { getSession } from '@/lib/auth'
10-
import { hasWorkspaceAccessControlAccess } from '@/lib/billing'
10+
import { isWorkspaceOnEnterprisePlan } from '@/lib/billing'
1111
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1212
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
1313

@@ -50,8 +50,8 @@ export const POST = withRouteHandler(
5050
return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 })
5151
}
5252

53-
const hasAccess = await hasWorkspaceAccessControlAccess(session.user.id, workspaceId)
54-
if (!hasAccess) {
53+
const entitled = await isWorkspaceOnEnterprisePlan(workspaceId)
54+
if (!entitled) {
5555
return NextResponse.json(
5656
{ error: 'Access Control is an Enterprise feature' },
5757
{ status: 403 }
@@ -97,36 +97,38 @@ export const POST = withRouteHandler(
9797
return NextResponse.json({ added: 0, moved: 0 })
9898
}
9999

100-
const existingMemberships = await db
101-
.select({
102-
id: permissionGroupMember.id,
103-
userId: permissionGroupMember.userId,
104-
permissionGroupId: permissionGroupMember.permissionGroupId,
105-
})
106-
.from(permissionGroupMember)
107-
.innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id))
108-
.where(
109-
and(
110-
eq(permissionGroup.workspaceId, workspaceId),
111-
inArray(permissionGroupMember.userId, targetUserIds)
100+
const { addedUserIds, movedCount } = await db.transaction(async (tx) => {
101+
const existingMemberships = await tx
102+
.select({
103+
id: permissionGroupMember.id,
104+
userId: permissionGroupMember.userId,
105+
permissionGroupId: permissionGroupMember.permissionGroupId,
106+
})
107+
.from(permissionGroupMember)
108+
.innerJoin(
109+
permissionGroup,
110+
eq(permissionGroupMember.permissionGroupId, permissionGroup.id)
111+
)
112+
.where(
113+
and(
114+
eq(permissionGroup.workspaceId, workspaceId),
115+
inArray(permissionGroupMember.userId, targetUserIds)
116+
)
112117
)
113-
)
114118

115-
const alreadyInThisGroup = new Set(
116-
existingMemberships.filter((m) => m.permissionGroupId === id).map((m) => m.userId)
117-
)
118-
const usersToAdd = targetUserIds.filter((uid) => !alreadyInThisGroup.has(uid))
119+
const alreadyInThisGroup = new Set(
120+
existingMemberships.filter((m) => m.permissionGroupId === id).map((m) => m.userId)
121+
)
122+
const usersToAdd = targetUserIds.filter((uid) => !alreadyInThisGroup.has(uid))
119123

120-
if (usersToAdd.length === 0) {
121-
return NextResponse.json({ added: 0, moved: 0 })
122-
}
124+
if (usersToAdd.length === 0) {
125+
return { addedUserIds: [] as string[], movedCount: 0 }
126+
}
123127

124-
const membershipsToDelete = existingMemberships.filter(
125-
(m) => m.permissionGroupId !== id && usersToAdd.includes(m.userId)
126-
)
127-
const movedCount = membershipsToDelete.length
128+
const membershipsToDelete = existingMemberships.filter(
129+
(m) => m.permissionGroupId !== id && usersToAdd.includes(m.userId)
130+
)
128131

129-
await db.transaction(async (tx) => {
130132
if (membershipsToDelete.length > 0) {
131133
await tx.delete(permissionGroupMember).where(
132134
inArray(
@@ -145,12 +147,18 @@ export const POST = withRouteHandler(
145147
}))
146148

147149
await tx.insert(permissionGroupMember).values(newMembers)
150+
151+
return { addedUserIds: usersToAdd, movedCount: membershipsToDelete.length }
148152
})
149153

154+
if (addedUserIds.length === 0) {
155+
return NextResponse.json({ added: 0, moved: 0 })
156+
}
157+
150158
logger.info('Bulk added members to permission group', {
151159
permissionGroupId: id,
152160
workspaceId,
153-
addedCount: usersToAdd.length,
161+
addedCount: addedUserIds.length,
154162
movedCount,
155163
assignedBy: session.user.id,
156164
})
@@ -164,16 +172,16 @@ export const POST = withRouteHandler(
164172
resourceName: group.name,
165173
actorName: session.user.name ?? undefined,
166174
actorEmail: session.user.email ?? undefined,
167-
description: `Bulk added ${usersToAdd.length} member(s) to permission group "${group.name}"`,
175+
description: `Bulk added ${addedUserIds.length} member(s) to permission group "${group.name}"`,
168176
metadata: {
169177
permissionGroupId: id,
170-
addedUserIds: usersToAdd,
178+
addedUserIds,
171179
movedCount,
172180
},
173181
request: req,
174182
})
175183

176-
return NextResponse.json({ added: usersToAdd.length, moved: movedCount })
184+
return NextResponse.json({ added: addedUserIds.length, moved: movedCount })
177185
} catch (error) {
178186
if (error instanceof z.ZodError) {
179187
return NextResponse.json({ error: error.errors[0].message }, { status: 400 })

apps/sim/app/api/workspaces/[workspaceId]/permission-groups/[id]/members/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server'
77
import { z } from 'zod'
88
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
99
import { getSession } from '@/lib/auth'
10-
import { hasWorkspaceAccessControlAccess, isWorkspaceOnEnterprisePlan } from '@/lib/billing'
10+
import { isWorkspaceOnEnterprisePlan } from '@/lib/billing'
1111
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1212
import { checkWorkspaceAccess, hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
1313

@@ -99,8 +99,8 @@ export const POST = withRouteHandler(
9999
return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 })
100100
}
101101

102-
const hasAccess = await hasWorkspaceAccessControlAccess(session.user.id, workspaceId)
103-
if (!hasAccess) {
102+
const entitled = await isWorkspaceOnEnterprisePlan(workspaceId)
103+
if (!entitled) {
104104
return NextResponse.json(
105105
{ error: 'Access Control is an Enterprise feature' },
106106
{ status: 403 }
@@ -244,8 +244,8 @@ export const DELETE = withRouteHandler(
244244
return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 })
245245
}
246246

247-
const hasAccess = await hasWorkspaceAccessControlAccess(session.user.id, workspaceId)
248-
if (!hasAccess) {
247+
const entitled = await isWorkspaceOnEnterprisePlan(workspaceId)
248+
if (!entitled) {
249249
return NextResponse.json(
250250
{ error: 'Access Control is an Enterprise feature' },
251251
{ status: 403 }

apps/sim/app/api/workspaces/[workspaceId]/permission-groups/[id]/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
88
import { getSession } from '@/lib/auth'
9-
import { hasWorkspaceAccessControlAccess, isWorkspaceOnEnterprisePlan } from '@/lib/billing'
9+
import { isWorkspaceOnEnterprisePlan } from '@/lib/billing'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import {
1212
type PermissionGroupConfig,
@@ -105,8 +105,8 @@ export const PUT = withRouteHandler(
105105
return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 })
106106
}
107107

108-
const hasAccess = await hasWorkspaceAccessControlAccess(session.user.id, workspaceId)
109-
if (!hasAccess) {
108+
const entitled = await isWorkspaceOnEnterprisePlan(workspaceId)
109+
if (!entitled) {
110110
return NextResponse.json(
111111
{ error: 'Access Control is an Enterprise feature' },
112112
{ status: 403 }
@@ -234,8 +234,8 @@ export const DELETE = withRouteHandler(
234234
return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 })
235235
}
236236

237-
const hasAccess = await hasWorkspaceAccessControlAccess(session.user.id, workspaceId)
238-
if (!hasAccess) {
237+
const entitled = await isWorkspaceOnEnterprisePlan(workspaceId)
238+
if (!entitled) {
239239
return NextResponse.json(
240240
{ error: 'Access Control is an Enterprise feature' },
241241
{ status: 403 }

apps/sim/app/api/workspaces/[workspaceId]/permission-groups/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server'
77
import { z } from 'zod'
88
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
99
import { getSession } from '@/lib/auth'
10-
import { hasWorkspaceAccessControlAccess, isWorkspaceOnEnterprisePlan } from '@/lib/billing'
10+
import { isWorkspaceOnEnterprisePlan } from '@/lib/billing'
1111
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1212
import {
1313
DEFAULT_PERMISSION_GROUP_CONFIG,
@@ -103,8 +103,8 @@ export const POST = withRouteHandler(
103103
return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 })
104104
}
105105

106-
const hasAccess = await hasWorkspaceAccessControlAccess(session.user.id, workspaceId)
107-
if (!hasAccess) {
106+
const entitled = await isWorkspaceOnEnterprisePlan(workspaceId)
107+
if (!entitled) {
108108
return NextResponse.json(
109109
{ error: 'Access Control is an Enterprise feature' },
110110
{ status: 403 }

apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,12 +399,7 @@ export function CredentialsManager() {
399399
const { data: workspacePermissions } = useWorkspacePermissionsQuery(workspaceId || null)
400400
const queryClient = useQueryClient()
401401

402-
const isWorkspaceAdmin = useMemo(() => {
403-
const userId = session?.user?.id
404-
if (!userId || !workspacePermissions?.users) return false
405-
const currentUser = workspacePermissions.users.find((user) => user.userId === userId)
406-
return currentUser?.permissionType === 'admin'
407-
}, [session?.user?.id, workspacePermissions?.users])
402+
const isWorkspaceAdmin = workspacePermissions?.viewer?.isAdmin ?? false
408403

409404
const isLoading = isPersonalLoading || isWorkspaceLoading
410405
const variables = useMemo(() => personalEnvData || {}, [personalEnvData])

apps/sim/app/workspace/[workspaceId]/settings/components/subscription/subscription.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,10 @@ export function Subscription() {
345345
const isCritical = isBlocked || usage.percentUsed >= USAGE_THRESHOLDS.CRITICAL
346346

347347
const billedAccountUserId = workspaceData?.settings?.workspace?.billedAccountUserId ?? null
348+
const workspaceMode = workspaceData?.settings?.workspace?.workspaceMode ?? null
348349
const isOrganizationWorkspace =
349-
workspaceData?.settings?.workspace?.organizationId != null &&
350-
workspaceData?.settings?.workspace?.workspaceMode === 'organization'
350+
workspaceData?.settings?.workspace?.organizationId != null && workspaceMode === 'organization'
351+
const isGrandfatheredSharedWorkspace = workspaceMode === 'grandfathered_shared'
351352
const workspaceAdmins: WorkspaceAdmin[] =
352353
workspaceData?.permissions?.users?.filter(
353354
(user: WorkspaceAdmin) => user.permissionType === 'admin'
@@ -1073,7 +1074,7 @@ export function Subscription() {
10731074
</>
10741075
)}
10751076

1076-
{!isLoading && isTeamAdmin && !isOrganizationWorkspace && (
1077+
{!isLoading && isTeamAdmin && isGrandfatheredSharedWorkspace && (
10771078
<div className='flex items-center justify-between gap-4'>
10781079
<div className='flex items-center gap-1.5'>
10791080
<Label htmlFor='billed-account'>Billed Account</Label>

apps/sim/ee/access-control/components/access-control.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
Switch,
2626
} from '@/components/emcn'
2727
import { Input as BaseInput } from '@/components/ui'
28-
import { useSession } from '@/lib/auth/auth-client'
2928
import { getEnv, isTruthy } from '@/lib/core/config/env'
3029
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
3130
import { getUserColor } from '@/lib/workspaces/colors'
@@ -245,7 +244,6 @@ function AccessControlSkeleton() {
245244
}
246245

247246
export function AccessControl() {
248-
const { data: session } = useSession()
249247
const params = useParams()
250248
const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : undefined
251249

@@ -259,12 +257,7 @@ export function AccessControl() {
259257
const { data: userPermissionConfig, isPending: entitlementLoading } =
260258
useUserPermissionConfig(workspaceId)
261259

262-
const currentUserIsWorkspaceAdmin = useMemo(() => {
263-
if (!workspacePermissionsData || !session?.user?.id) return false
264-
return workspacePermissionsData.users.some(
265-
(u) => u.userId === session.user?.id && u.permissionType === 'admin'
266-
)
267-
}, [workspacePermissionsData, session?.user?.id])
260+
const currentUserIsWorkspaceAdmin = workspacePermissionsData?.viewer?.isAdmin ?? false
268261

269262
const accessControlEnabledLocally = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED'))
270263
const isEntitled = accessControlEnabledLocally || !!userPermissionConfig?.entitled

apps/sim/hooks/queries/workspace.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,22 @@ export interface WorkspaceUser {
253253
permissionType: 'admin' | 'write' | 'read'
254254
}
255255

256+
/** Viewer context for a workspace permissions response. */
257+
export interface WorkspacePermissionsViewer {
258+
userId: string
259+
/**
260+
* Mirrors the server's `hasWorkspaceAdminAccess` check: true when the caller is the workspace
261+
* owner, an explicit workspace admin, or an organization owner/admin of an organization-owned
262+
* workspace. Use this instead of scanning `users` for a `permissionType === 'admin'` row.
263+
*/
264+
isAdmin: boolean
265+
}
266+
256267
/** Workspace permissions data containing all users and their access levels. */
257268
export interface WorkspacePermissions {
258269
users: WorkspaceUser[]
259270
total: number
271+
viewer?: WorkspacePermissionsViewer
260272
}
261273

262274
async function fetchWorkspacePermissions(

0 commit comments

Comments
 (0)