Skip to content

Commit 47eb060

Browse files
feat(enterprise): permission groups, access control (#2736)
* feat(permission-groups): integration/model access controls for enterprise * feat: enterprise gating for BYOK, SSO, credential sets with org admin/owner checks * execution time enforcement of mcp and custom tools * add admin routes to cleanup permission group data * fix not being on enterprise checks * separate out orgs from billing system * update the docs * add custom tool blockers based on perm configs * add migrations * fix * address greptile comments * regen migrations * fix default model picking based on user config * cleaned up UI
1 parent fd76e98 commit 47eb060

File tree

67 files changed

+13669
-478
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+13669
-478
lines changed

apps/docs/content/docs/en/enterprise/index.mdx

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Enterprise
3-
description: Enterprise features for organizations with advanced security and compliance requirements
3+
description: Enterprise features for business organizations
44
---
55

66
import { Callout } from 'fumadocs-ui/components/callout'
@@ -9,6 +9,28 @@ Sim Studio Enterprise provides advanced features for organizations with enhanced
99

1010
---
1111

12+
## Access Control
13+
14+
Define permission groups to control what features and integrations team members can use.
15+
16+
### Features
17+
18+
- **Allowed Model Providers** - Restrict which AI providers users can access (OpenAI, Anthropic, Google, etc.)
19+
- **Allowed Blocks** - Control which workflow blocks are available
20+
- **Platform Settings** - Hide Knowledge Base, disable MCP tools, or disable custom tools
21+
22+
### Setup
23+
24+
1. Navigate to **Settings****Access Control** in your workspace
25+
2. Create a permission group with your desired restrictions
26+
3. Add team members to the permission group
27+
28+
<Callout type="info">
29+
Users not assigned to any permission group have full access. Permission restrictions are enforced at both UI and execution time.
30+
</Callout>
31+
32+
---
33+
1234
## Bring Your Own Key (BYOK)
1335

1436
Use your own API keys for AI model providers instead of Sim Studio's hosted keys.
@@ -61,15 +83,38 @@ Enterprise authentication with SAML 2.0 and OIDC support for centralized identit
6183

6284
---
6385

64-
## Self-Hosted
86+
## Self-Hosted Configuration
87+
88+
For self-hosted deployments, enterprise features can be enabled via environment variables without requiring billing.
6589

66-
For self-hosted deployments, enterprise features can be enabled via environment variables:
90+
### Environment Variables
6791

6892
| Variable | Description |
6993
|----------|-------------|
94+
| `ORGANIZATIONS_ENABLED`, `NEXT_PUBLIC_ORGANIZATIONS_ENABLED` | Enable team/organization management |
95+
| `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions |
7096
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC |
7197
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers |
7298

73-
<Callout type="warn">
74-
BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables.
75-
</Callout>
99+
### Organization Management
100+
101+
When billing is disabled, use the Admin API to manage organizations:
102+
103+
```bash
104+
# Create an organization
105+
curl -X POST https://your-instance/api/v1/admin/organizations \
106+
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
107+
-H "Content-Type: application/json" \
108+
-d '{"name": "My Organization", "ownerId": "user-id-here"}'
109+
110+
# Add a member
111+
curl -X POST https://your-instance/api/v1/admin/organizations/{orgId}/members \
112+
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
113+
-H "Content-Type: application/json" \
114+
-d '{"userId": "user-id-here", "role": "admin"}'
115+
```
116+
117+
### Notes
118+
119+
- Enabling `ACCESS_CONTROL_ENABLED` automatically enables organizations, as access control requires organization membership.
120+
- BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { db } from '@sim/db'
2+
import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { and, eq, inArray } from 'drizzle-orm'
5+
import { type NextRequest, NextResponse } from 'next/server'
6+
import { z } from 'zod'
7+
import { getSession } from '@/lib/auth'
8+
import { hasAccessControlAccess } from '@/lib/billing'
9+
10+
const logger = createLogger('PermissionGroupBulkMembers')
11+
12+
async function getPermissionGroupWithAccess(groupId: string, userId: string) {
13+
const [group] = await db
14+
.select({
15+
id: permissionGroup.id,
16+
organizationId: permissionGroup.organizationId,
17+
})
18+
.from(permissionGroup)
19+
.where(eq(permissionGroup.id, groupId))
20+
.limit(1)
21+
22+
if (!group) return null
23+
24+
const [membership] = await db
25+
.select({ role: member.role })
26+
.from(member)
27+
.where(and(eq(member.userId, userId), eq(member.organizationId, group.organizationId)))
28+
.limit(1)
29+
30+
if (!membership) return null
31+
32+
return { group, role: membership.role }
33+
}
34+
35+
const bulkAddSchema = z.object({
36+
userIds: z.array(z.string()).optional(),
37+
addAllOrgMembers: z.boolean().optional(),
38+
})
39+
40+
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
41+
const session = await getSession()
42+
43+
if (!session?.user?.id) {
44+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
45+
}
46+
47+
const { id } = await params
48+
49+
try {
50+
const hasAccess = await hasAccessControlAccess(session.user.id)
51+
if (!hasAccess) {
52+
return NextResponse.json(
53+
{ error: 'Access Control is an Enterprise feature' },
54+
{ status: 403 }
55+
)
56+
}
57+
58+
const result = await getPermissionGroupWithAccess(id, session.user.id)
59+
60+
if (!result) {
61+
return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
62+
}
63+
64+
if (result.role !== 'admin' && result.role !== 'owner') {
65+
return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 })
66+
}
67+
68+
const body = await req.json()
69+
const { userIds, addAllOrgMembers } = bulkAddSchema.parse(body)
70+
71+
let targetUserIds: string[] = []
72+
73+
if (addAllOrgMembers) {
74+
const orgMembers = await db
75+
.select({ userId: member.userId })
76+
.from(member)
77+
.where(eq(member.organizationId, result.group.organizationId))
78+
79+
targetUserIds = orgMembers.map((m) => m.userId)
80+
} else if (userIds && userIds.length > 0) {
81+
const validMembers = await db
82+
.select({ userId: member.userId })
83+
.from(member)
84+
.where(
85+
and(
86+
eq(member.organizationId, result.group.organizationId),
87+
inArray(member.userId, userIds)
88+
)
89+
)
90+
91+
targetUserIds = validMembers.map((m) => m.userId)
92+
}
93+
94+
if (targetUserIds.length === 0) {
95+
return NextResponse.json({ added: 0, moved: 0 })
96+
}
97+
98+
const existingMemberships = await db
99+
.select({
100+
id: permissionGroupMember.id,
101+
userId: permissionGroupMember.userId,
102+
permissionGroupId: permissionGroupMember.permissionGroupId,
103+
})
104+
.from(permissionGroupMember)
105+
.where(inArray(permissionGroupMember.userId, targetUserIds))
106+
107+
const alreadyInThisGroup = new Set(
108+
existingMemberships.filter((m) => m.permissionGroupId === id).map((m) => m.userId)
109+
)
110+
const usersToAdd = targetUserIds.filter((uid) => !alreadyInThisGroup.has(uid))
111+
112+
if (usersToAdd.length === 0) {
113+
return NextResponse.json({ added: 0, moved: 0 })
114+
}
115+
116+
const membershipsToDelete = existingMemberships.filter(
117+
(m) => m.permissionGroupId !== id && usersToAdd.includes(m.userId)
118+
)
119+
const movedCount = membershipsToDelete.length
120+
121+
await db.transaction(async (tx) => {
122+
if (membershipsToDelete.length > 0) {
123+
await tx.delete(permissionGroupMember).where(
124+
inArray(
125+
permissionGroupMember.id,
126+
membershipsToDelete.map((m) => m.id)
127+
)
128+
)
129+
}
130+
131+
const newMembers = usersToAdd.map((userId) => ({
132+
id: crypto.randomUUID(),
133+
permissionGroupId: id,
134+
userId,
135+
assignedBy: session.user.id,
136+
assignedAt: new Date(),
137+
}))
138+
139+
await tx.insert(permissionGroupMember).values(newMembers)
140+
})
141+
142+
logger.info('Bulk added members to permission group', {
143+
permissionGroupId: id,
144+
addedCount: usersToAdd.length,
145+
movedCount,
146+
assignedBy: session.user.id,
147+
})
148+
149+
return NextResponse.json({ added: usersToAdd.length, moved: movedCount })
150+
} catch (error) {
151+
if (error instanceof z.ZodError) {
152+
return NextResponse.json({ error: error.errors[0].message }, { status: 400 })
153+
}
154+
if (
155+
error instanceof Error &&
156+
error.message.includes('permission_group_member_user_id_unique')
157+
) {
158+
return NextResponse.json(
159+
{ error: 'One or more users are already in a permission group' },
160+
{ status: 409 }
161+
)
162+
}
163+
logger.error('Error bulk adding members to permission group', error)
164+
return NextResponse.json({ error: 'Failed to add members' }, { status: 500 })
165+
}
166+
}

0 commit comments

Comments
 (0)