Skip to content

Commit a18cbf3

Browse files
fix(credential-groups): delete removed enrollments
1 parent 51afa70 commit a18cbf3

12 files changed

Lines changed: 115 additions & 79 deletions

File tree

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import { revokeCredentialGroupEnrollmentContract } from '@/lib/api/contracts/credential-groups'
1+
import { deleteCredentialGroupEnrollmentContract } from '@/lib/api/contracts/credential-groups'
22
import {
33
defineInternalJsonRoute,
44
internalRateLimits,
55
internalSessionAuth,
66
} from '@/lib/api/server/routes'
7-
import { revokeCredentialGroupEnrollmentSettings } from '@/lib/credential-groups/application/manage-enrollments'
7+
import { deleteCredentialGroupEnrollmentSettings } from '@/lib/credential-groups/application/manage-enrollments'
88
import { credentialGroupOperations } from '@/lib/credential-groups/application/operations'
99
import { createCredentialGroupInternalErrorPolicy } from '@/app/api/workspaces/[id]/credential-groups/error-policy'
1010

1111
export const DELETE = defineInternalJsonRoute({
12-
contract: revokeCredentialGroupEnrollmentContract,
12+
contract: deleteCredentialGroupEnrollmentContract,
1313
auth: internalSessionAuth,
14-
operation: credentialGroupOperations.revokeEnrollment,
14+
operation: credentialGroupOperations.deleteEnrollment,
1515
rateLimit: internalRateLimits.none({
16-
reason: 'Preserve existing internal Credential Group revocation behavior',
16+
reason: 'Preserve existing internal Credential Group deletion behavior',
1717
}),
1818
errorPolicy: createCredentialGroupInternalErrorPolicy(
1919
'Failed to delete person from credential group'
@@ -23,5 +23,5 @@ export const DELETE = defineInternalJsonRoute({
2323
credentialGroupId: params.groupId,
2424
enrollmentId: params.enrollmentId,
2525
}),
26-
useCase: revokeCredentialGroupEnrollmentSettings,
26+
useCase: deleteCredentialGroupEnrollmentSettings,
2727
})

apps/sim/ee/credential-groups/components/credential-group-detail.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ import { CredentialGroupInviteModal } from '@/ee/credential-groups/components/cr
3333
import {
3434
useCredentialGroupDetail,
3535
useDeleteCredentialGroup,
36+
useDeleteCredentialGroupEnrollment,
3637
useResendCredentialGroupEnrollment,
37-
useRevokeCredentialGroupEnrollment,
3838
useUpdateCredentialGroup,
3939
} from '@/hooks/queries/credential-groups'
4040
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
@@ -94,7 +94,7 @@ export function CredentialGroupDetail({
9494
providerId: SLACK_CUSTOM_BOT_PROVIDER_ID,
9595
})
9696
const resend = useResendCredentialGroupEnrollment()
97-
const deleteEnrollment = useRevokeCredentialGroupEnrollment()
97+
const deleteEnrollment = useDeleteCredentialGroupEnrollment()
9898
const updateGroup = useUpdateCredentialGroup()
9999
const deleteGroup = useDeleteCredentialGroup()
100100
const [activeTab, setActiveTab] = useQueryState(credentialGroupTabParam.key, {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import type { ContractBodyInput } from '@/lib/api/contracts'
66
import {
77
createCredentialGroupContract,
88
deleteCredentialGroupContract,
9+
deleteCredentialGroupEnrollmentContract,
910
getCredentialGroupContract,
1011
inviteCredentialGroupEnrollmentsContract,
1112
resendCredentialGroupEnrollmentContract,
12-
revokeCredentialGroupEnrollmentContract,
1313
startSlackCredentialGroupConfigurationContract,
1414
updateCredentialGroupContract,
1515
} from '@/lib/api/contracts/credential-groups'
@@ -184,7 +184,7 @@ export function useResendCredentialGroupEnrollment() {
184184
})
185185
}
186186

187-
export function useRevokeCredentialGroupEnrollment() {
187+
export function useDeleteCredentialGroupEnrollment() {
188188
const queryClient = useQueryClient()
189189
return useMutation({
190190
mutationFn: async ({
@@ -196,7 +196,7 @@ export function useRevokeCredentialGroupEnrollment() {
196196
groupId: string
197197
enrollmentId: string
198198
}) =>
199-
requestJson(revokeCredentialGroupEnrollmentContract, {
199+
requestJson(deleteCredentialGroupEnrollmentContract, {
200200
params: { id: workspaceId, groupId, enrollmentId },
201201
}),
202202
onSettled: (_data, _error, variables) => {

apps/sim/lib/api/contracts/credential-groups.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ export const resendCredentialGroupEnrollmentContract = defineRouteContract({
351351
},
352352
})
353353

354-
export const revokeCredentialGroupEnrollmentContract = defineRouteContract({
354+
export const deleteCredentialGroupEnrollmentContract = defineRouteContract({
355355
method: 'DELETE',
356356
path: '/api/workspaces/[id]/credential-groups/[groupId]/enrollments/[enrollmentId]',
357357
params: credentialGroupEnrollmentParamsSchema,

apps/sim/lib/credential-groups/application/list-people.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ export const listCredentialGroupPeople = defineAuthorizedWorkspaceUseCase({
6969
} catch (error) {
7070
if (error instanceof CredentialGroupEnrollmentError) {
7171
throw new OrchestrationError(
72-
error.status === 404 ? 'validation' : error.status === 409 ? 'conflict' : 'internal',
72+
error.status === 400 || error.status === 404
73+
? 'validation'
74+
: error.status === 409
75+
? 'conflict'
76+
: 'internal',
7377
error.message
7478
)
7579
}

apps/sim/lib/credential-groups/application/manage-enrollments.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ vi.mock('@/lib/credential-groups/enrollments', () => ({
2121
CredentialGroupEnrollmentError: class CredentialGroupEnrollmentError extends Error {
2222
constructor(
2323
message: string,
24-
readonly status: 404 | 409 | 502
24+
readonly status: 400 | 404 | 409 | 502
2525
) {
2626
super(message)
2727
}
2828
},
29+
deleteCredentialGroupEnrollment: vi.fn(),
2930
inviteCredentialGroupEnrollments: mocks.invite,
3031
loadCredentialGroupInviterIdentity: mocks.loadInviter,
3132
resendCredentialGroupEnrollment: vi.fn(),
32-
revokeCredentialGroupEnrollment: vi.fn(),
3333
}))
3434

3535
vi.mock('@sim/platform-authz/workspace', () => ({

apps/sim/lib/credential-groups/application/manage-enrollments.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { credentialGroupOperations } from '@/lib/credential-groups/application/o
99
import { validateCredentialGroupInvitationEmails } from '@/lib/credential-groups/application/validation'
1010
import {
1111
CredentialGroupEnrollmentError,
12+
deleteCredentialGroupEnrollment,
1213
inviteCredentialGroupEnrollments,
1314
loadCredentialGroupInviterIdentity,
1415
resendCredentialGroupEnrollment,
15-
revokeCredentialGroupEnrollment,
1616
} from '@/lib/credential-groups/enrollments'
1717

1818
interface CredentialGroupEnrollmentSettingsInput {
@@ -109,20 +109,20 @@ export const resendCredentialGroupEnrollmentSettings = defineAuthorizedWorkspace
109109
}),
110110
})
111111

112-
export interface RevokeCredentialGroupEnrollmentSettingsInput
112+
export interface DeleteCredentialGroupEnrollmentSettingsInput
113113
extends CredentialGroupEnrollmentSettingsInput {
114114
enrollmentId: string
115115
}
116116

117-
export const revokeCredentialGroupEnrollmentSettings = defineAuthorizedWorkspaceUseCase({
118-
operation: credentialGroupOperations.revokeEnrollment,
119-
resolveContext: ({ input }: { input: RevokeCredentialGroupEnrollmentSettingsInput }) =>
117+
export const deleteCredentialGroupEnrollmentSettings = defineAuthorizedWorkspaceUseCase({
118+
operation: credentialGroupOperations.deleteEnrollment,
119+
resolveContext: ({ input }: { input: DeleteCredentialGroupEnrollmentSettingsInput }) =>
120120
resolveCredentialGroupSettingsContext(input.credentialGroupId, input.assertedWorkspaceId),
121121
authorizationOptions: {},
122122
async execute({ input, context }) {
123123
await requireCredentialGroupSettingsAvailable(context.workspaceId)
124124
try {
125-
const credentialGroupEnrollment = await revokeCredentialGroupEnrollment(
125+
const credentialGroupEnrollment = await deleteCredentialGroupEnrollment(
126126
context.workspaceId,
127127
context.credentialGroupId,
128128
input.enrollmentId
@@ -137,7 +137,7 @@ export const revokeCredentialGroupEnrollmentSettings = defineAuthorizedWorkspace
137137
resourceType: AuditResourceType.CREDENTIAL_GROUP,
138138
resourceId: context.credentialGroupId,
139139
resourceName: context.name,
140-
description: `Revoked Credential Group access for ${result.credentialGroupEnrollment.email}`,
140+
description: `Deleted ${result.credentialGroupEnrollment.email} from the Credential Group`,
141141
metadata: { enrollmentId: result.credentialGroupEnrollment.id },
142142
}),
143143
})

apps/sim/lib/credential-groups/application/operations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export const credentialGroupOperations = {
4343
workspaceApiKey: 'deny',
4444
principalKinds: ['session'],
4545
}),
46-
revokeEnrollment: defineWorkspaceOperation({
47-
id: 'credential_groups.enrollments.revoke',
46+
deleteEnrollment: defineWorkspaceOperation({
47+
id: 'credential_groups.enrollments.delete',
4848
minimumRole: 'admin',
4949
workspaceApiKey: 'deny',
5050
principalKinds: ['session'],

apps/sim/lib/credential-groups/enrollments.test.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ vi.mock('@/lib/credential-groups/provider-registry', () => ({
3232

3333
import {
3434
completeCredentialGroupEnrollment,
35+
deleteCredentialGroupEnrollment,
3536
listCredentialGroupEnrollments,
3637
resendCredentialGroupEnrollment,
37-
revokeCredentialGroupEnrollment,
3838
} from '@/lib/credential-groups/enrollments'
3939
import { CREDENTIAL_GROUP_PROVIDER_IDS } from '@/lib/credential-groups/providers'
4040
import { sendEmail } from '@/lib/messaging/email/mailer'
@@ -143,29 +143,43 @@ describe('listCredentialGroupEnrollments', () => {
143143
}
144144
dbChainMockFns.limit
145145
.mockResolvedValueOnce([{ options: [] }])
146-
.mockResolvedValueOnce([
147-
{ id: ENROLLMENT.id, invitedAt: new Date('2026-08-11T12:00:00.000Z') },
148-
])
146+
.mockResolvedValueOnce([{ enrollment: ENROLLMENT }, { enrollment: remainingEnrollment }])
147+
.mockResolvedValueOnce([{ options: [] }])
149148
.mockResolvedValueOnce([{ enrollment: remainingEnrollment }])
150149

150+
const firstPage = await listCredentialGroupEnrollments('workspace-1', 'group-1', 1, undefined, {
151+
statuses: ['invited', 'in_progress', 'completed', 'delivery_failed'],
152+
})
153+
if (!firstPage.nextCursor) throw new Error('Expected a next enrollment cursor')
151154
const result = await listCredentialGroupEnrollments(
152155
'workspace-1',
153156
'group-1',
154157
50,
155-
ENROLLMENT.id,
158+
firstPage.nextCursor,
156159
{ statuses: ['invited', 'in_progress', 'completed', 'delivery_failed'] }
157160
)
158161

162+
expect(firstPage.nextCursor).toEqual(expect.any(String))
159163
expect(result.enrollments).toHaveLength(1)
160164
expect(result.enrollments[0]?.id).toBe(remainingEnrollment.id)
161-
expect(inArray).toHaveBeenCalledOnce()
165+
expect(dbChainMockFns.limit).toHaveBeenCalledTimes(4)
162166
expect(inArray).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment.status, [
163167
'invited',
164168
'in_progress',
165169
'completed',
166170
'delivery_failed',
167171
])
168172
})
173+
174+
it('rejects a malformed enrollment cursor', async () => {
175+
dbChainMockFns.limit.mockResolvedValueOnce([{ options: [] }])
176+
177+
await expect(
178+
listCredentialGroupEnrollments('workspace-1', 'group-1', 50, 'not-a-cursor')
179+
).rejects.toMatchObject({ message: 'Enrollment cursor is invalid', status: 400 })
180+
181+
expect(dbChainMockFns.limit).toHaveBeenCalledOnce()
182+
})
169183
})
170184

171185
describe('resendCredentialGroupEnrollment', () => {
@@ -236,25 +250,22 @@ describe('resendCredentialGroupEnrollment', () => {
236250
})
237251
})
238252

239-
describe('revokeCredentialGroupEnrollment', () => {
253+
describe('deleteCredentialGroupEnrollment', () => {
240254
beforeEach(() => {
241255
vi.clearAllMocks()
242256
resetDbChainMock()
243257
})
244258

245-
it('deletes every managed credential collected under the revoked enrollment', async () => {
259+
it('deletes the enrollment and lets its foreign-key cascade remove managed credentials', async () => {
246260
dbChainMockFns.limit.mockResolvedValueOnce([{ email: ENROLLMENT.email }])
247-
dbChainMockFns.returning.mockResolvedValueOnce([
248-
{ ...ENROLLMENT, status: 'revoked', revokedAt: new Date() },
249-
])
261+
dbChainMockFns.returning.mockResolvedValueOnce([ENROLLMENT])
250262

251-
const result = await revokeCredentialGroupEnrollment('workspace-1', 'group-1', ENROLLMENT.id)
263+
const result = await deleteCredentialGroupEnrollment('workspace-1', 'group-1', ENROLLMENT.id)
252264

253-
expect(result.status).toBe('revoked')
254-
expect(dbChainMockFns.update).toHaveBeenCalledOnce()
255-
expect(dbChainMockFns.update).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment)
265+
expect(result.id).toBe(ENROLLMENT.id)
266+
expect(dbChainMockFns.update).not.toHaveBeenCalled()
256267
expect(dbChainMockFns.delete).toHaveBeenCalledOnce()
257-
expect(dbChainMockFns.delete).toHaveBeenCalledWith(schemaMock.credential)
268+
expect(dbChainMockFns.delete).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment)
258269
})
259270
})
260271

0 commit comments

Comments
 (0)