Skip to content

Commit 62aa462

Browse files
committed
improvement(permissions): added ability to auto-add new org members to existing permission group, disallow disabling of start block
1 parent 929d0d0 commit 62aa462

File tree

7 files changed

+164
-6
lines changed

7 files changed

+164
-6
lines changed

apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
invitation,
55
member,
66
organization,
7+
permissionGroup,
8+
permissionGroupMember,
79
permissions,
810
subscription as subscriptionTable,
911
user,
@@ -17,6 +19,7 @@ import { type NextRequest, NextResponse } from 'next/server'
1719
import { z } from 'zod'
1820
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
1921
import { getSession } from '@/lib/auth'
22+
import { hasAccessControlAccess } from '@/lib/billing'
2023
import { requireStripeClient } from '@/lib/billing/stripe-client'
2124
import { getBaseUrl } from '@/lib/core/utils/urls'
2225
import { sendEmail } from '@/lib/messaging/email/mailer'
@@ -382,6 +385,47 @@ export async function PUT(
382385
// Don't fail the whole invitation acceptance due to this
383386
}
384387

388+
// Auto-assign to permission group if one has autoAddNewMembers enabled
389+
try {
390+
const hasAccessControl = await hasAccessControlAccess(session.user.id)
391+
if (hasAccessControl) {
392+
const [autoAddGroup] = await tx
393+
.select({ id: permissionGroup.id, name: permissionGroup.name })
394+
.from(permissionGroup)
395+
.where(
396+
and(
397+
eq(permissionGroup.organizationId, organizationId),
398+
eq(permissionGroup.autoAddNewMembers, true)
399+
)
400+
)
401+
.limit(1)
402+
403+
if (autoAddGroup) {
404+
await tx.insert(permissionGroupMember).values({
405+
id: randomUUID(),
406+
permissionGroupId: autoAddGroup.id,
407+
userId: session.user.id,
408+
assignedBy: null,
409+
assignedAt: new Date(),
410+
})
411+
412+
logger.info('Auto-assigned new member to permission group', {
413+
userId: session.user.id,
414+
organizationId,
415+
permissionGroupId: autoAddGroup.id,
416+
permissionGroupName: autoAddGroup.name,
417+
})
418+
}
419+
}
420+
} catch (error) {
421+
logger.error('Failed to auto-assign user to permission group', {
422+
userId: session.user.id,
423+
organizationId,
424+
error,
425+
})
426+
// Don't fail the whole invitation acceptance due to this
427+
}
428+
385429
const linkedWorkspaceInvitations = await tx
386430
.select()
387431
.from(workspaceInvitation)

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const updateSchema = z.object({
3131
name: z.string().trim().min(1).max(100).optional(),
3232
description: z.string().max(500).nullable().optional(),
3333
config: configSchema.optional(),
34+
autoAddNewMembers: z.boolean().optional(),
3435
})
3536

3637
async function getPermissionGroupWithAccess(groupId: string, userId: string) {
@@ -44,6 +45,7 @@ async function getPermissionGroupWithAccess(groupId: string, userId: string) {
4445
createdBy: permissionGroup.createdBy,
4546
createdAt: permissionGroup.createdAt,
4647
updatedAt: permissionGroup.updatedAt,
48+
autoAddNewMembers: permissionGroup.autoAddNewMembers,
4749
})
4850
.from(permissionGroup)
4951
.where(eq(permissionGroup.id, groupId))
@@ -140,11 +142,27 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
140142
? { ...currentConfig, ...updates.config }
141143
: currentConfig
142144

145+
// If setting autoAddNewMembers to true, unset it on other groups in the org first
146+
if (updates.autoAddNewMembers === true) {
147+
await db
148+
.update(permissionGroup)
149+
.set({ autoAddNewMembers: false, updatedAt: new Date() })
150+
.where(
151+
and(
152+
eq(permissionGroup.organizationId, result.group.organizationId),
153+
eq(permissionGroup.autoAddNewMembers, true)
154+
)
155+
)
156+
}
157+
143158
await db
144159
.update(permissionGroup)
145160
.set({
146161
...(updates.name !== undefined && { name: updates.name }),
147162
...(updates.description !== undefined && { description: updates.description }),
163+
...(updates.autoAddNewMembers !== undefined && {
164+
autoAddNewMembers: updates.autoAddNewMembers,
165+
}),
148166
config: newConfig,
149167
updatedAt: new Date(),
150168
})

apps/sim/app/api/permission-groups/route.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const createSchema = z.object({
3333
name: z.string().trim().min(1).max(100),
3434
description: z.string().max(500).optional(),
3535
config: configSchema.optional(),
36+
autoAddNewMembers: z.boolean().optional(),
3637
})
3738

3839
export async function GET(req: Request) {
@@ -68,6 +69,7 @@ export async function GET(req: Request) {
6869
createdBy: permissionGroup.createdBy,
6970
createdAt: permissionGroup.createdAt,
7071
updatedAt: permissionGroup.updatedAt,
72+
autoAddNewMembers: permissionGroup.autoAddNewMembers,
7173
creatorName: user.name,
7274
creatorEmail: user.email,
7375
})
@@ -111,7 +113,8 @@ export async function POST(req: Request) {
111113
}
112114

113115
const body = await req.json()
114-
const { organizationId, name, description, config } = createSchema.parse(body)
116+
const { organizationId, name, description, config, autoAddNewMembers } =
117+
createSchema.parse(body)
115118

116119
const membership = await db
117120
.select({ id: member.id, role: member.role })
@@ -154,6 +157,19 @@ export async function POST(req: Request) {
154157
...config,
155158
}
156159

160+
// If autoAddNewMembers is true, unset it on any existing groups first
161+
if (autoAddNewMembers) {
162+
await db
163+
.update(permissionGroup)
164+
.set({ autoAddNewMembers: false, updatedAt: new Date() })
165+
.where(
166+
and(
167+
eq(permissionGroup.organizationId, organizationId),
168+
eq(permissionGroup.autoAddNewMembers, true)
169+
)
170+
)
171+
}
172+
157173
const now = new Date()
158174
const newGroup = {
159175
id: crypto.randomUUID(),
@@ -164,6 +180,7 @@ export async function POST(req: Request) {
164180
createdBy: session.user.id,
165181
createdAt: now,
166182
updatedAt: now,
183+
autoAddNewMembers: autoAddNewMembers || false,
167184
}
168185

169186
await db.insert(permissionGroup).values(newGroup)

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export function AccessControl() {
268268
const [viewingGroup, setViewingGroup] = useState<PermissionGroup | null>(null)
269269
const [newGroupName, setNewGroupName] = useState('')
270270
const [newGroupDescription, setNewGroupDescription] = useState('')
271+
const [newGroupAutoAdd, setNewGroupAutoAdd] = useState(false)
271272
const [createError, setCreateError] = useState<string | null>(null)
272273
const [deletingGroup, setDeletingGroup] = useState<{ id: string; name: string } | null>(null)
273274
const [deletingGroupIds, setDeletingGroupIds] = useState<Set<string>>(new Set())
@@ -417,14 +418,16 @@ export function AccessControl() {
417418
if (!newGroupName.trim() || !activeOrganization?.id) return
418419
setCreateError(null)
419420
try {
420-
const result = await createPermissionGroup.mutateAsync({
421+
await createPermissionGroup.mutateAsync({
421422
organizationId: activeOrganization.id,
422423
name: newGroupName.trim(),
423424
description: newGroupDescription.trim() || undefined,
425+
autoAddNewMembers: newGroupAutoAdd,
424426
})
425427
setShowCreateModal(false)
426428
setNewGroupName('')
427429
setNewGroupDescription('')
430+
setNewGroupAutoAdd(false)
428431
} catch (error) {
429432
logger.error('Failed to create permission group', error)
430433
if (error instanceof Error) {
@@ -433,12 +436,19 @@ export function AccessControl() {
433436
setCreateError('Failed to create permission group')
434437
}
435438
}
436-
}, [newGroupName, newGroupDescription, activeOrganization?.id, createPermissionGroup])
439+
}, [
440+
newGroupName,
441+
newGroupDescription,
442+
newGroupAutoAdd,
443+
activeOrganization?.id,
444+
createPermissionGroup,
445+
])
437446

438447
const handleCloseCreateModal = useCallback(() => {
439448
setShowCreateModal(false)
440449
setNewGroupName('')
441450
setNewGroupDescription('')
451+
setNewGroupAutoAdd(false)
442452
setCreateError(null)
443453
}, [])
444454

@@ -533,6 +543,23 @@ export function AccessControl() {
533543
}
534544
}, [viewingGroup, selectedMemberIds, bulkAddMembers])
535545

546+
const handleToggleAutoAdd = useCallback(
547+
async (enabled: boolean) => {
548+
if (!viewingGroup || !activeOrganization?.id) return
549+
try {
550+
await updatePermissionGroup.mutateAsync({
551+
id: viewingGroup.id,
552+
organizationId: activeOrganization.id,
553+
autoAddNewMembers: enabled,
554+
})
555+
setViewingGroup((prev) => (prev ? { ...prev, autoAddNewMembers: enabled } : null))
556+
} catch (error) {
557+
logger.error('Failed to toggle auto-add', error)
558+
}
559+
},
560+
[viewingGroup, activeOrganization?.id, updatePermissionGroup]
561+
)
562+
536563
const toggleIntegration = useCallback(
537564
(blockType: string) => {
538565
if (!editingConfig) return
@@ -630,6 +657,22 @@ export function AccessControl() {
630657
)}
631658
</div>
632659

660+
<div className='flex items-center justify-between rounded-[8px] border border-[var(--border)] px-[12px] py-[10px]'>
661+
<div className='flex flex-col gap-[2px]'>
662+
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
663+
Auto-add new members
664+
</span>
665+
<span className='text-[12px] text-[var(--text-muted)]'>
666+
Automatically add new organization members to this group
667+
</span>
668+
</div>
669+
<Checkbox
670+
checked={viewingGroup.autoAddNewMembers}
671+
onCheckedChange={(checked) => handleToggleAutoAdd(checked === true)}
672+
disabled={updatePermissionGroup.isPending}
673+
/>
674+
</div>
675+
633676
<div className='min-h-0 flex-1 overflow-y-auto'>
634677
<div className='flex flex-col gap-[16px]'>
635678
<div className='flex items-center justify-between'>
@@ -814,7 +857,13 @@ export function AccessControl() {
814857
editingConfig?.allowedIntegrations === null ||
815858
editingConfig?.allowedIntegrations?.length === allBlocks.length
816859
setEditingConfig((prev) =>
817-
prev ? { ...prev, allowedIntegrations: allAllowed ? [] : null } : prev
860+
prev
861+
? {
862+
...prev,
863+
// When deselecting all, keep start_trigger allowed (it should never be disabled)
864+
allowedIntegrations: allAllowed ? ['start_trigger'] : null,
865+
}
866+
: prev
818867
)
819868
}}
820869
>
@@ -1058,7 +1107,14 @@ export function AccessControl() {
10581107
{filteredGroups.map((group) => (
10591108
<div key={group.id} className='flex items-center justify-between'>
10601109
<div className='flex flex-col'>
1061-
<span className='font-medium text-[14px]'>{group.name}</span>
1110+
<div className='flex items-center gap-[8px]'>
1111+
<span className='font-medium text-[14px]'>{group.name}</span>
1112+
{group.autoAddNewMembers && (
1113+
<span className='rounded-[4px] bg-[var(--surface-3)] px-[6px] py-[2px] text-[10px] text-[var(--text-muted)]'>
1114+
Auto-enrolls
1115+
</span>
1116+
)}
1117+
</div>
10621118
<span className='text-[13px] text-[var(--text-muted)]'>
10631119
{group.memberCount} member{group.memberCount !== 1 ? 's' : ''}
10641120
</span>
@@ -1106,6 +1162,16 @@ export function AccessControl() {
11061162
placeholder='e.g., Limited access for marketing users'
11071163
/>
11081164
</div>
1165+
<div className='flex items-center gap-[8px]'>
1166+
<Checkbox
1167+
id='auto-add-members'
1168+
checked={newGroupAutoAdd}
1169+
onCheckedChange={(checked) => setNewGroupAutoAdd(checked === true)}
1170+
/>
1171+
<Label htmlFor='auto-add-members' className='cursor-pointer font-normal'>
1172+
Auto-add new organization members
1173+
</Label>
1174+
</div>
11091175
{createError && <p className='text-[12px] text-[var(--text-error)]'>{createError}</p>}
11101176
</div>
11111177
</ModalBody>

apps/sim/hooks/queries/permission-groups.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface PermissionGroup {
1313
creatorName: string | null
1414
creatorEmail: string | null
1515
memberCount: number
16+
autoAddNewMembers: boolean
1617
}
1718

1819
export interface PermissionGroupMember {
@@ -111,6 +112,7 @@ export interface CreatePermissionGroupData {
111112
name: string
112113
description?: string
113114
config?: Partial<PermissionGroupConfig>
115+
autoAddNewMembers?: boolean
114116
}
115117

116118
export function useCreatePermissionGroup() {
@@ -143,6 +145,7 @@ export interface UpdatePermissionGroupData {
143145
name?: string
144146
description?: string | null
145147
config?: Partial<PermissionGroupConfig>
148+
autoAddNewMembers?: boolean
146149
}
147150

148151
export function useUpdatePermissionGroup() {

apps/sim/hooks/use-permission-config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export function usePermissionConfig(): PermissionConfigResult {
3535

3636
const isBlockAllowed = useMemo(() => {
3737
return (blockType: string) => {
38+
// start_trigger should always be allowed (it should never be disabled)
39+
if (blockType === 'start_trigger') return true
3840
if (config.allowedIntegrations === null) return true
3941
return config.allowedIntegrations.includes(blockType)
4042
}
@@ -50,7 +52,11 @@ export function usePermissionConfig(): PermissionConfigResult {
5052
const filterBlocks = useMemo(() => {
5153
return <T extends { type: string }>(blocks: T[]): T[] => {
5254
if (config.allowedIntegrations === null) return blocks
53-
return blocks.filter((block) => config.allowedIntegrations!.includes(block.type))
55+
// start_trigger should always be included (it should never be disabled)
56+
return blocks.filter(
57+
(block) =>
58+
block.type === 'start_trigger' || config.allowedIntegrations!.includes(block.type)
59+
)
5460
}
5561
}, [config.allowedIntegrations])
5662

packages/db/schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2078,6 +2078,7 @@ export const permissionGroup = pgTable(
20782078
.references(() => user.id, { onDelete: 'cascade' }),
20792079
createdAt: timestamp('created_at').notNull().defaultNow(),
20802080
updatedAt: timestamp('updated_at').notNull().defaultNow(),
2081+
autoAddNewMembers: boolean('auto_add_new_members').notNull().default(false),
20812082
},
20822083
(table) => ({
20832084
organizationIdIdx: index('permission_group_organization_id_idx').on(table.organizationId),
@@ -2086,6 +2087,9 @@ export const permissionGroup = pgTable(
20862087
table.organizationId,
20872088
table.name
20882089
),
2090+
autoAddNewMembersUnique: uniqueIndex('permission_group_org_auto_add_unique')
2091+
.on(table.organizationId)
2092+
.where(sql`auto_add_new_members = true`),
20892093
})
20902094
)
20912095

0 commit comments

Comments
 (0)