Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions apps/docs/content/docs/en/enterprise/index.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Enterprise
description: Enterprise features for organizations with advanced security and compliance requirements
description: Enterprise features for business organizations
---

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

---

## Access Control

Define permission groups to control what features and integrations team members can use.

### Features

- **Allowed Model Providers** - Restrict which AI providers users can access (OpenAI, Anthropic, Google, etc.)
- **Allowed Blocks** - Control which workflow blocks are available
- **Platform Settings** - Hide Knowledge Base, disable MCP tools, or disable custom tools

### Setup

1. Navigate to **Settings** → **Access Control** in your workspace
2. Create a permission group with your desired restrictions
3. Add team members to the permission group

<Callout type="info">
Users not assigned to any permission group have full access. Permission restrictions are enforced at both UI and execution time.
</Callout>

---

## Bring Your Own Key (BYOK)

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

---

## Self-Hosted
## Self-Hosted Configuration

For self-hosted deployments, enterprise features can be enabled via environment variables without requiring billing.

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

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

<Callout type="warn">
BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables.
</Callout>
### Organization Management

When billing is disabled, use the Admin API to manage organizations:

```bash
# Create an organization
curl -X POST https://your-instance/api/v1/admin/organizations \
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My Organization", "ownerId": "user-id-here"}'

# Add a member
curl -X POST https://your-instance/api/v1/admin/organizations/{orgId}/members \
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "user-id-here", "role": "admin"}'
```

### Notes

- Enabling `ACCESS_CONTROL_ENABLED` automatically enables organizations, as access control requires organization membership.
- BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables.
162 changes: 162 additions & 0 deletions apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { db } from '@sim/db'
import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, inArray, notInArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { hasAccessControlAccess } from '@/lib/billing'

const logger = createLogger('PermissionGroupBulkMembers')

async function getPermissionGroupWithAccess(groupId: string, userId: string) {
const [group] = await db
.select({
id: permissionGroup.id,
organizationId: permissionGroup.organizationId,
})
.from(permissionGroup)
.where(eq(permissionGroup.id, groupId))
.limit(1)

if (!group) return null

const [membership] = await db
.select({ role: member.role })
.from(member)
.where(and(eq(member.userId, userId), eq(member.organizationId, group.organizationId)))
.limit(1)

if (!membership) return null

return { group, role: membership.role }
}

const bulkAddSchema = z.object({
userIds: z.array(z.string()).optional(),
addAllOrgMembers: z.boolean().optional(),
})

export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getSession()

if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const { id } = await params

try {
const hasAccess = await hasAccessControlAccess(session.user.id)
if (!hasAccess) {
return NextResponse.json(
{ error: 'Access Control is an Enterprise feature' },
{ status: 403 }
)
}

const result = await getPermissionGroupWithAccess(id, session.user.id)

if (!result) {
return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
}

if (result.role !== 'admin' && result.role !== 'owner') {
return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 })
}

const body = await req.json()
const { userIds, addAllOrgMembers } = bulkAddSchema.parse(body)

let targetUserIds: string[] = []

if (addAllOrgMembers) {
const orgMembers = await db
.select({ userId: member.userId })
.from(member)
.where(eq(member.organizationId, result.group.organizationId))

targetUserIds = orgMembers.map((m) => m.userId)
} else if (userIds && userIds.length > 0) {
const validMembers = await db
.select({ userId: member.userId })
.from(member)
.where(
and(
eq(member.organizationId, result.group.organizationId),
inArray(member.userId, userIds)
)
)

targetUserIds = validMembers.map((m) => m.userId)
}

if (targetUserIds.length === 0) {
return NextResponse.json({ added: 0, moved: 0 })
}

const existingInThisGroup = await db
.select({ userId: permissionGroupMember.userId })
.from(permissionGroupMember)
.where(
and(
eq(permissionGroupMember.permissionGroupId, id),
inArray(permissionGroupMember.userId, targetUserIds)
)
)

const existingUserIds = new Set(existingInThisGroup.map((m) => m.userId))
const usersToAdd = targetUserIds.filter((uid) => !existingUserIds.has(uid))

if (usersToAdd.length === 0) {
return NextResponse.json({ added: 0, moved: 0 })
}

const otherGroupMemberships = await db
.select({
id: permissionGroupMember.id,
userId: permissionGroupMember.userId,
})
.from(permissionGroupMember)
.innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id))
.where(
and(
eq(permissionGroup.organizationId, result.group.organizationId),
inArray(permissionGroupMember.userId, usersToAdd),
notInArray(permissionGroupMember.permissionGroupId, [id])
)
)

const movedCount = otherGroupMemberships.length

if (otherGroupMemberships.length > 0) {
const idsToDelete = otherGroupMemberships.map((m) => m.id)
await db.delete(permissionGroupMember).where(inArray(permissionGroupMember.id, idsToDelete))
}

const newMembers = usersToAdd.map((userId) => ({
id: crypto.randomUUID(),
permissionGroupId: id,
userId,
assignedBy: session.user.id,
assignedAt: new Date(),
}))

await db.insert(permissionGroupMember).values(newMembers)

logger.info('Bulk added members to permission group', {
permissionGroupId: id,
addedCount: usersToAdd.length,
movedCount,
assignedBy: session.user.id,
})

return NextResponse.json({ added: usersToAdd.length, moved: movedCount })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.errors[0].message }, { status: 400 })
}
logger.error('Error bulk adding members to permission group', error)
return NextResponse.json({ error: 'Failed to add members' }, { status: 500 })
}
}
Loading
Loading