mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-01 14:59:19 +08:00
fix(credentials): hide gated service accounts from connected list (#6898)
This commit is contained in:
@@ -20,6 +20,9 @@ const {
|
||||
mockCheckWorkspaceAccess,
|
||||
mockGetCredentialActorContext,
|
||||
mockGetCredentialCreationWorkspaceContext,
|
||||
mockGetBlockVisibility,
|
||||
mockCreateIntegrationCredentialVisibility,
|
||||
mockIsCredentialVisible,
|
||||
mockLoadWorkspace,
|
||||
mockResolveWorkspacePermission,
|
||||
mockSyncWorkspaceOAuthCredentials,
|
||||
@@ -28,6 +31,9 @@ const {
|
||||
mockCheckWorkspaceAccess: vi.fn(),
|
||||
mockGetCredentialActorContext: vi.fn(),
|
||||
mockGetCredentialCreationWorkspaceContext: vi.fn(),
|
||||
mockGetBlockVisibility: vi.fn(),
|
||||
mockCreateIntegrationCredentialVisibility: vi.fn(),
|
||||
mockIsCredentialVisible: vi.fn(),
|
||||
mockLoadWorkspace: vi.fn(),
|
||||
mockResolveWorkspacePermission: vi.fn(),
|
||||
mockSyncWorkspaceOAuthCredentials: vi.fn(),
|
||||
@@ -37,6 +43,14 @@ const {
|
||||
vi.mock('@sim/audit', () => auditMock)
|
||||
vi.mock('@/lib/posthog/server', () => posthogServerMock)
|
||||
|
||||
vi.mock('@/lib/core/config/block-visibility', () => ({
|
||||
getBlockVisibility: mockGetBlockVisibility,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/integrations/credential-visibility.server', () => ({
|
||||
createIntegrationCredentialVisibility: mockCreateIntegrationCredentialVisibility,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workspaces/permissions/utils', () => ({
|
||||
checkWorkspaceAccess: mockCheckWorkspaceAccess,
|
||||
}))
|
||||
@@ -109,6 +123,16 @@ describe('GET /api/credentials', () => {
|
||||
canWrite: true,
|
||||
canAdmin: false,
|
||||
})
|
||||
mockGetBlockVisibility.mockResolvedValue({
|
||||
revealed: new Set(),
|
||||
disabled: new Set(),
|
||||
previewTagged: new Set(),
|
||||
})
|
||||
mockIsCredentialVisible.mockReturnValue(true)
|
||||
mockCreateIntegrationCredentialVisibility.mockReturnValue({
|
||||
isCredentialVisible: mockIsCredentialVisible,
|
||||
isOAuthServiceVisible: vi.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
it('reports an owned personal secret as raw-view admin without a membership row', async () => {
|
||||
@@ -150,6 +174,70 @@ describe('GET /api/credentials', () => {
|
||||
role: 'admin',
|
||||
}),
|
||||
])
|
||||
expect(mockGetBlockVisibility).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides a service-account credential when its gating block is preview-hidden', async () => {
|
||||
mockIsCredentialVisible.mockReturnValue(false)
|
||||
queueTableRows(credential, [
|
||||
{
|
||||
id: 'slack-credential',
|
||||
workspaceId: WORKSPACE_ID,
|
||||
type: 'service_account',
|
||||
displayName: 'Slack custom bot',
|
||||
description: null,
|
||||
providerId: 'slack-custom-bot',
|
||||
accountId: null,
|
||||
envKey: null,
|
||||
envOwnerUserId: null,
|
||||
createdBy: 'user-1',
|
||||
createdAt: new Date('2026-08-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-08-01T00:00:00.000Z'),
|
||||
memberRole: 'admin',
|
||||
},
|
||||
{
|
||||
id: 'google-credential',
|
||||
workspaceId: WORKSPACE_ID,
|
||||
type: 'oauth',
|
||||
displayName: 'Google account',
|
||||
description: null,
|
||||
providerId: 'google-email',
|
||||
accountId: 'google-account',
|
||||
envKey: null,
|
||||
envOwnerUserId: null,
|
||||
createdBy: 'user-1',
|
||||
createdAt: new Date('2026-08-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-08-01T00:00:00.000Z'),
|
||||
memberRole: 'admin',
|
||||
},
|
||||
])
|
||||
|
||||
const response = await GET(
|
||||
createMockRequest(
|
||||
'GET',
|
||||
undefined,
|
||||
{},
|
||||
`http://localhost:3000/api/credentials?workspaceId=${WORKSPACE_ID}`
|
||||
)
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({
|
||||
credentials: [expect.objectContaining({ id: 'google-credential' })],
|
||||
})
|
||||
expect(mockGetBlockVisibility).toHaveBeenCalledWith({ userId: 'user-1', orgId: 'org-1' })
|
||||
expect(mockCreateIntegrationCredentialVisibility).toHaveBeenCalledWith({
|
||||
allowedIntegrationTypes: null,
|
||||
blockVisibility: {
|
||||
revealed: new Set(),
|
||||
disabled: new Set(),
|
||||
previewTagged: new Set(),
|
||||
},
|
||||
})
|
||||
expect(mockIsCredentialVisible).toHaveBeenCalledExactlyOnceWith({
|
||||
providerId: 'slack-custom-bot',
|
||||
type: 'service_account',
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes padded, blank, and duplicate legacy query values', async () => {
|
||||
@@ -197,6 +285,7 @@ describe('GET /api/credentials', () => {
|
||||
})
|
||||
expect(mockSyncWorkspaceOAuthCredentials).not.toHaveBeenCalled()
|
||||
expect(mockCheckWorkspaceAccess).not.toHaveBeenCalled()
|
||||
expect(mockGetBlockVisibility).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AuditAction, AuditResourceType } from '@sim/audit'
|
||||
import { requirePrincipalSubjectUserId } from '@sim/auth/principal'
|
||||
import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application'
|
||||
import { getBlockVisibility } from '@/lib/core/config/block-visibility'
|
||||
import { OrchestrationError } from '@/lib/core/orchestration/types'
|
||||
import { canUseCredential, getCredentialActorContext } from '@/lib/credentials/access'
|
||||
import {
|
||||
@@ -25,6 +26,8 @@ import {
|
||||
type VisibleWorkspaceCredential,
|
||||
type WorkspaceCredentialLookup,
|
||||
} from '@/lib/credentials/queries'
|
||||
import { getServiceAccountGatingBlockType } from '@/lib/credentials/service-account-provider-ids'
|
||||
import { createIntegrationCredentialVisibility } from '@/lib/integrations/credential-visibility.server'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import { loadActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/workspace-context'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
@@ -77,6 +80,39 @@ export type ListInternalCredentialsResult =
|
||||
| { mode: 'list'; credentials: VisibleWorkspaceCredential[] }
|
||||
| { mode: 'lookup'; credential: WorkspaceCredentialLookup | null }
|
||||
|
||||
async function filterGatedServiceAccountCredentials(
|
||||
credentials: VisibleWorkspaceCredential[],
|
||||
viewer: { userId: string; organizationId: string | null }
|
||||
): Promise<VisibleWorkspaceCredential[]> {
|
||||
const gatedProviderIds = new Set(
|
||||
credentials.flatMap(({ providerId }) => {
|
||||
if (!providerId || !getServiceAccountGatingBlockType(providerId)) return []
|
||||
return [providerId]
|
||||
})
|
||||
)
|
||||
if (gatedProviderIds.size === 0) return credentials
|
||||
|
||||
const blockVisibility = await getBlockVisibility({
|
||||
userId: viewer.userId,
|
||||
...(viewer.organizationId ? { orgId: viewer.organizationId } : {}),
|
||||
})
|
||||
const visibility = createIntegrationCredentialVisibility({
|
||||
allowedIntegrationTypes: null,
|
||||
blockVisibility,
|
||||
})
|
||||
|
||||
return credentials.filter((credential) => {
|
||||
const { providerId } = credential
|
||||
if (!providerId || !gatedProviderIds.has(providerId)) return true
|
||||
if (credential.type !== 'service_account') {
|
||||
throw new Error(
|
||||
`Gated service-account provider ${providerId} has credential type ${credential.type}`
|
||||
)
|
||||
}
|
||||
return visibility.isCredentialVisible({ providerId, type: credential.type })
|
||||
})
|
||||
}
|
||||
|
||||
export const listInternalCredentials = defineAuthorizedWorkspaceUseCase({
|
||||
operation: credentialOperations.listInternal,
|
||||
resolveContext: async ({ input }: { input: ListInternalCredentialsInput }) => {
|
||||
@@ -108,7 +144,11 @@ export const listInternalCredentials = defineAuthorizedWorkspaceUseCase({
|
||||
types: input.type ? [input.type] : undefined,
|
||||
providerId: input.providerId,
|
||||
})
|
||||
return { mode: 'list', credentials: page.data }
|
||||
const credentials = await filterGatedServiceAccountCredentials(page.data, {
|
||||
userId,
|
||||
organizationId: context.workspaceOrganizationId,
|
||||
})
|
||||
return { mode: 'list', credentials }
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user