diff --git a/apps/sim/lib/credentials/application/organization-credentials.ts b/apps/sim/lib/credentials/application/organization-credentials.ts index 599ada36385..82d8bbe900d 100644 --- a/apps/sim/lib/credentials/application/organization-credentials.ts +++ b/apps/sim/lib/credentials/application/organization-credentials.ts @@ -263,6 +263,7 @@ export async function authorizeOrganizationCredentialUse(input: { const row = await getOrganizationCredential(input.organizationId, input.credentialId) if ( !row || + row.revokedAt || (row.type !== 'oauth' && row.type !== 'service_account') || !row.providerId || (row.type === 'oauth' && row.createdBy !== context.userId) diff --git a/apps/sim/lib/knowledge/application/connector-access.ts b/apps/sim/lib/knowledge/application/connector-access.ts index aded446945e..c2986b43410 100644 --- a/apps/sim/lib/knowledge/application/connector-access.ts +++ b/apps/sim/lib/knowledge/application/connector-access.ts @@ -234,6 +234,9 @@ export const updateKnowledgeConnectorAccess = defineAuthorizedKnowledgeUseCase({ } const previousConfig = connector.sourceConfig as Record const sourceConfig = await prepareGitHubInstallationSource({ + principal, + requestId, + workspaceId: context.workspaceId, connectorType: connector.connectorType, credentialId: input.credentialId === undefined && input.accessMode === connector.accessMode @@ -271,6 +274,7 @@ export const updateKnowledgeConnectorAccess = defineAuthorizedKnowledgeUseCase({ } if (credentialId) { await requireUsableCredential({ + principal, credentialId, connectorMeta, sourceConfig, @@ -280,6 +284,7 @@ export const updateKnowledgeConnectorAccess = defineAuthorizedKnowledgeUseCase({ accessMode: 'members', }) const rejection = await validateConnectorSourceConfig({ + principal, connector: { ...connector, accessMode: 'members', credentialId }, sourceConfig, ...owner, @@ -299,6 +304,7 @@ export const updateKnowledgeConnectorAccess = defineAuthorizedKnowledgeUseCase({ target = { accessMode: input.accessMode, credentialId: await requireUsableCredential({ + principal, credentialId: input.credentialId, connectorMeta, sourceConfig, @@ -309,6 +315,7 @@ export const updateKnowledgeConnectorAccess = defineAuthorizedKnowledgeUseCase({ }), } const rejection = await validateConnectorSourceConfig({ + principal, connector: { ...connector, accessMode: target.accessMode, @@ -372,6 +379,7 @@ export const updateKnowledgeConnectorAccess = defineAuthorizedKnowledgeUseCase({ * source validation verifies it against the target mode before any mutation. */ async function requireUsableCredential(input: { + principal: Principal credentialId: string | null | undefined connectorMeta: Pick sourceConfig: Record @@ -403,6 +411,7 @@ async function requireUsableCredential(input: { ) } const token = await resolveConnectorCredentialAccessToken({ + principal: input.principal, credentialId: input.credentialId, ...resourceScopeFields(resourceScopeFromOwner(input)), actingUserId: input.actingUserId, diff --git a/apps/sim/lib/knowledge/application/connector-credential.test.ts b/apps/sim/lib/knowledge/application/connector-credential.test.ts new file mode 100644 index 00000000000..5852292eb70 --- /dev/null +++ b/apps/sim/lib/knowledge/application/connector-credential.test.ts @@ -0,0 +1,198 @@ +/** @vitest-environment node */ +import type { Principal } from '@sim/auth/principal' +import { credential, member } from '@sim/db/schema' +import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' +import { and, eq, isNull } from 'drizzle-orm' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + config: vi.fn(), + catalog: vi.fn(), + requireService: vi.fn(), + requireOAuth: vi.fn(), + repository: vi.fn(), +})) +vi.mock('@/lib/permission-groups/resolve.server', () => ({ + getUserPermissionConfigForOrganization: mocks.config, +})) +vi.mock('@/lib/credentials/application/provider-catalog', () => ({ + listCredentialProviderCatalog: mocks.catalog, + requireAvailableServiceAccountCredentialProvider: mocks.requireService, + requireAvailableOAuthCredentialProvider: mocks.requireOAuth, +})) +vi.mock('@/lib/credentials/application/credential-crud', () => ({ + throwCredentialMutationFailure: vi.fn(), +})) +vi.mock('@/lib/credentials/orchestration/credential-create', () => ({ + createCredentialRecord: vi.fn(), +})) +vi.mock('@/lib/credentials/orchestration', () => ({ updateCredentialRecord: vi.fn() })) +vi.mock('@/lib/credentials/connect-draft', () => ({ + createConnectDraft: vi.fn(), + getActiveConnectDraft: vi.fn(), +})) +vi.mock('@/lib/oauth/credential-service', () => ({ resolveCredentialTokenBundle: vi.fn() })) +vi.mock('@/lib/core/security/encryption', () => ({ + decryptSecret: async () => ({ decrypted: '{}' }), +})) +vi.mock('@/lib/oauth/github-installation', () => ({ + parseGitHubInstallationBinding: () => ({ installationId: '42', accountId: '7' }), + resolveGitHubInstallationRepository: mocks.repository, +})) + +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { requireConnectorCredential } from '@/lib/knowledge/application/connector-credential' +import { prepareGitHubInstallationSource } from '@/lib/knowledge/application/github-installation-source' +import { DEFAULT_PERMISSION_GROUP_CONFIG } from '@/lib/permission-groups/fields' + +const principal: Principal = { kind: 'session', userId: 'admin-1', sessionId: 'session-1' } +const installed = { + id: 'installation-credential', + organizationId: 'org-1', + workspaceId: null, + type: 'service_account', + providerId: 'github-app-installation', + createdBy: 'installer-1', + revokedAt: null, + encryptedServiceAccountKey: 'encrypted', + providerSubjectId: '42', + providerTenantId: '7', +} +const input = { + principal, + credentialId: installed.id, + scope: { kind: 'organization' as const, organizationId: 'org-1' }, + actingUserId: 'admin-1', + requestId: 'request-1', +} + +beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mocks.config.mockResolvedValue(null) + mocks.catalog.mockResolvedValue([]) + mocks.repository.mockResolvedValue({ id: '123', fullName: 'example/private' }) +}) + +describe('organization source credential authorization', () => { + it.each(['owner', 'admin'])('pins an installation repository for a current %s', async (role) => { + queueTableRows(member, [{ role }]) + queueTableRows(credential, [installed]) + + await expect( + prepareGitHubInstallationSource({ + principal, + requestId: input.requestId, + connectorType: 'github', + credentialId: installed.id, + organizationId: 'org-1', + isSearchIndex: true, + accessMode: 'members', + actingUserId: 'admin-1', + sourceConfig: { repository: 'example/private' }, + }) + ).resolves.toEqual({ repository: 'example/private', githubRepositoryId: '123' }) + expect(dbChainMockFns.where).toHaveBeenCalledWith( + and(eq(member.organizationId, 'org-1'), eq(member.userId, 'admin-1')) + ) + expect(dbChainMockFns.where).toHaveBeenCalledWith( + and( + eq(credential.id, installed.id), + and(eq(credential.organizationId, 'org-1'), isNull(credential.workspaceId)) + ) + ) + expect(mocks.requireService).toHaveBeenCalledWith([], 'github-app-installation') + }) + + it.each([{ rows: [] }, { rows: [{ role: 'member' }] }])( + 'refuses missing or insufficient membership: %j', + async ({ rows }) => { + queueTableRows(member, rows) + queueTableRows(credential, [installed]) + await expect(requireConnectorCredential(input)).rejects.toBeInstanceOf(OrchestrationError) + expect(dbChainMockFns.from).not.toHaveBeenCalledWith(credential) + expect(mocks.catalog).not.toHaveBeenCalled() + } + ) + + it('does not substitute the credential creator or attributed user for the principal', async () => { + queueTableRows(member, []) + await expect( + requireConnectorCredential({ ...input, actingUserId: installed.createdBy }) + ).rejects.toMatchObject({ code: 'not_found' }) + expect(dbChainMockFns.where).toHaveBeenCalledWith( + and(eq(member.organizationId, 'org-1'), eq(member.userId, principal.userId)) + ) + }) + + it('refuses a credential outside the asserted organization', async () => { + queueTableRows(member, [{ role: 'admin' }]) + queueTableRows(credential, []) + await expect(requireConnectorCredential(input)).rejects.toMatchObject({ code: 'not_found' }) + expect(mocks.catalog).not.toHaveBeenCalled() + }) + + it('does not make an organization credential usable from a workspace', async () => { + queueTableRows(credential, [installed]) + await expect( + requireConnectorCredential({ ...input, scope: { kind: 'workspace', workspaceId: 'ws-1' } }) + ).rejects.toMatchObject({ code: 'validation' }) + expect(mocks.catalog).not.toHaveBeenCalled() + }) + + it('refuses revoked credentials before provider access', async () => { + queueTableRows(member, [{ role: 'admin' }]) + queueTableRows(credential, [{ ...installed, revokedAt: new Date() }]) + await expect(requireConnectorCredential(input)).rejects.toMatchObject({ code: 'not_found' }) + expect(mocks.catalog).not.toHaveBeenCalled() + }) + + it('does not let an admin use another person’s OAuth account', async () => { + queueTableRows(member, [{ role: 'admin' }]) + queueTableRows(credential, [{ ...installed, type: 'oauth', providerId: 'github-repositories' }]) + await expect(requireConnectorCredential(input)).rejects.toMatchObject({ code: 'not_found' }) + }) + + it('allows an admin to use their own organization OAuth account', async () => { + const ownAccount = { + ...installed, + type: 'oauth', + providerId: 'github-repositories', + createdBy: 'admin-1', + } + queueTableRows(member, [{ role: 'admin' }]) + queueTableRows(credential, [ownAccount]) + await expect(requireConnectorCredential(input)).resolves.toEqual(ownAccount) + expect(mocks.requireOAuth).toHaveBeenCalledWith([], 'github-repositories') + }) + + it('enforces the existing integration-management capability', async () => { + queueTableRows(member, [{ role: 'admin' }]) + mocks.config.mockResolvedValue({ + ...DEFAULT_PERMISSION_GROUP_CONFIG, + hideIntegrationsTab: true, + }) + await expect(requireConnectorCredential(input)).rejects.toMatchObject({ code: 'forbidden' }) + expect(dbChainMockFns.from).not.toHaveBeenCalledWith(credential) + }) + + it('refuses workspace keys before protected loading', async () => { + await expect( + requireConnectorCredential({ + ...input, + principal: { kind: 'workspace_api_key', workspaceId: 'ws-1', keyId: 'key-1' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + expect(dbChainMockFns.from).not.toHaveBeenCalled() + }) + + it('propagates provider policy denials', async () => { + queueTableRows(member, [{ role: 'admin' }]) + queueTableRows(credential, [installed]) + const denial = new OrchestrationError('forbidden', 'Provider is unavailable') + mocks.requireService.mockImplementationOnce(() => { + throw denial + }) + await expect(requireConnectorCredential(input)).rejects.toBe(denial) + }) +}) diff --git a/apps/sim/lib/knowledge/application/connector-credential.ts b/apps/sim/lib/knowledge/application/connector-credential.ts new file mode 100644 index 00000000000..514917095df --- /dev/null +++ b/apps/sim/lib/knowledge/application/connector-credential.ts @@ -0,0 +1,42 @@ +import type { Principal } from '@sim/auth/principal' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { + type ResourceScope, + resourceScopeFromOwner, + sameResourceScope, +} from '@/lib/core/resource-scope' +import { canUseCredential, getCredentialActorContext } from '@/lib/credentials/access' +import { authorizeOrganizationCredentialUse } from '@/lib/credentials/application/organization-credentials' +import type { CredentialRow } from '@/lib/credentials/queries' + +/** Resolves source credentials using the authorization policy of their canonical owner scope. */ +export async function requireConnectorCredential(input: { + principal: Principal + credentialId: string + scope: ResourceScope + actingUserId: string + requestId: string +}): Promise { + if (input.scope.kind === 'organization') { + const { credential } = await authorizeOrganizationCredentialUse({ + principal: input.principal, + organizationId: input.scope.organizationId, + credentialId: input.credentialId, + requestId: input.requestId, + }) + return credential + } + + const access = await getCredentialActorContext(input.credentialId, input.actingUserId) + if ( + !access.credential || + !sameResourceScope(resourceScopeFromOwner(access.credential), input.scope) || + !canUseCredential(access) + ) { + throw new OrchestrationError( + 'validation', + 'Credential is not available to you in this workspace. Ask a credential administrator to grant access or select another credential.' + ) + } + return access.credential +} diff --git a/apps/sim/lib/knowledge/application/connectors.test.ts b/apps/sim/lib/knowledge/application/connectors.test.ts index 652c8b40e18..e36e516f69f 100644 --- a/apps/sim/lib/knowledge/application/connectors.test.ts +++ b/apps/sim/lib/knowledge/application/connectors.test.ts @@ -268,6 +268,7 @@ describe('knowledge connector application use cases', () => { async (accessMode) => { await expect( resolveConnectorCredentialAccessToken({ + principal: { kind: 'session', userId: 'admin', sessionId: 'session' }, credentialId: 'credential-1', workspaceId: 'workspace-a', actingUserId: 'admin', @@ -285,6 +286,7 @@ describe('knowledge connector application use cases', () => { mocks.resolveTokenIdentity.mockResolvedValueOnce({ kind: 'service_account' }) await expect( resolveConnectorCredentialAccessToken({ + principal: { kind: 'session', userId: 'admin', sessionId: 'session' }, credentialId: 'credential-1', workspaceId: 'workspace-a', actingUserId: 'admin', @@ -316,6 +318,7 @@ describe('knowledge connector application use cases', () => { } as Parameters[0]['connector'] await expect( validateConnectorSourceConfig({ + principal: { kind: 'session', userId: 'admin', sessionId: 'session' }, connector, sourceConfig: { adminEmail: 'admin@corp.com' }, workspaceId: 'workspace-a', diff --git a/apps/sim/lib/knowledge/application/connectors.ts b/apps/sim/lib/knowledge/application/connectors.ts index e8adeb13283..e98a4817de6 100644 --- a/apps/sim/lib/knowledge/application/connectors.ts +++ b/apps/sim/lib/knowledge/application/connectors.ts @@ -25,14 +25,9 @@ import { type ResourceScope, resourceScopeFields, resourceScopeFromOwner, - sameResourceScope, } from '@/lib/core/resource-scope' import { generateRequestId } from '@/lib/core/utils/request' -import { - canUseCredential, - getCredentialActorContext, - resolveCredentialTokenIdentity, -} from '@/lib/credentials/access' +import { resolveCredentialTokenIdentity } from '@/lib/credentials/access' import { requireKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability' import { knowledgeAccessCondition } from '@/lib/knowledge/access/predicate' import { createKnowledgeAccessProvider } from '@/lib/knowledge/access/scope' @@ -42,6 +37,7 @@ import { resolveKnowledgeAttributedUserId, resolveKnowledgeBillingAttribution, } from '@/lib/knowledge/application/billing' +import { requireConnectorCredential } from '@/lib/knowledge/application/connector-credential' import { type ActiveKnowledgeResourceBaseContext, resolveActiveKnowledgeConnectorContext, @@ -243,6 +239,8 @@ export function requireConnectorWorkspaceId(context: ActiveKnowledgeResourceBase } async function resolveAuthorizedConnectorCredentialIdentity(input: { + principal: Principal + requestId: string credentialId: string workspaceId?: string organizationId?: string @@ -251,21 +249,14 @@ async function resolveAuthorizedConnectorCredentialIdentity(input: { auth: ConnectorAuthConfig accessMode: string }) { - const access = await getCredentialActorContext(input.credentialId, input.actingUserId) - if ( - !access.credential || - !sameResourceScope(resourceScopeFromOwner(access.credential), resourceScopeFromOwner(input)) || - !canUseCredential(access) - ) { - throw new OrchestrationError( - 'validation', - 'Credential is not available to you in this workspace. Ask a credential administrator to grant access or select another credential.' - ) - } + const credential = await requireConnectorCredential({ + ...input, + scope: resourceScopeFromOwner(input), + }) if ( input.service && - (!access.credential.providerId || - !credentialProviderMatchesService(access.credential.providerId, input.service)) + (!credential.providerId || + !credentialProviderMatchesService(credential.providerId, input.service)) ) { throw new OrchestrationError( 'validation', @@ -291,6 +282,7 @@ async function resolveAuthorizedConnectorCredentialIdentity(input: { * of another provider. */ export async function resolveConnectorCredentialAccessToken(input: { + principal: Principal credentialId: string workspaceId?: string organizationId?: string @@ -315,6 +307,7 @@ export async function resolveConnectorCredentialAccessToken(input: { } export async function validateConnectorSourceConfig(input: { + principal: Principal connector: KnowledgeConnectorRow sourceConfig: Record workspaceId?: string @@ -375,6 +368,8 @@ export async function validateConnectorSourceConfig(input: { } } const identity = await resolveAuthorizedConnectorCredentialIdentity({ + principal: input.principal, + requestId: input.requestId, credentialId: input.connector.credentialId, workspaceId: input.workspaceId, organizationId: input.organizationId, @@ -767,6 +762,9 @@ async function executeCreateKnowledgeConnector( } } const sourceConfig = await prepareGitHubInstallationSource({ + principal, + requestId, + workspaceId: context.workspaceId, connectorType: input.connectorType, credentialId: input.credentialId, organizationId: context.organizationId, @@ -792,6 +790,7 @@ async function executeCreateKnowledgeConnector( resolveKnowledgeBillingAttribution(principal, context), resolveAccessToken: (credentialId) => resolveConnectorCredentialAccessToken({ + principal, credentialId, ...owner, actingUserId, @@ -941,6 +940,9 @@ export const updateKnowledgeConnector = defineAuthorizedKnowledgeUseCase({ updates: input.updates, prepareSourceConfig: (connector, sourceConfig) => prepareGitHubInstallationSource({ + principal, + requestId, + workspaceId: context.workspaceId, connectorType: connector.connectorType, credentialId: connector.credentialId, organizationId: context.organizationId, @@ -960,6 +962,7 @@ export const updateKnowledgeConnector = defineAuthorizedKnowledgeUseCase({ validateSourceConfig: (connector, sourceConfig) => { const owner = resourceScopeFields(resourceScopeFromOwner(context)) return validateConnectorSourceConfig({ + principal, connector, sourceConfig, ...owner, diff --git a/apps/sim/lib/knowledge/application/github-installation-source.test.ts b/apps/sim/lib/knowledge/application/github-installation-source.test.ts index 5eaee58ae6c..2c7666faf04 100644 --- a/apps/sim/lib/knowledge/application/github-installation-source.test.ts +++ b/apps/sim/lib/knowledge/application/github-installation-source.test.ts @@ -3,14 +3,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const m = vi.hoisted(() => ({ access: vi.fn(), - canUse: vi.fn(), decrypt: vi.fn(), parse: vi.fn(), repository: vi.fn(), })) -vi.mock('@/lib/credentials/access', () => ({ - getCredentialActorContext: m.access, - canUseCredential: m.canUse, +vi.mock('@/lib/knowledge/application/connector-credential', () => ({ + requireConnectorCredential: m.access, })) vi.mock('@/lib/core/security/encryption', () => ({ decryptSecret: m.decrypt })) vi.mock('@/lib/oauth/github-installation', () => ({ @@ -18,6 +16,7 @@ vi.mock('@/lib/oauth/github-installation', () => ({ resolveGitHubInstallationRepository: m.repository, })) +import { OrchestrationError } from '@/lib/core/orchestration/types' import { prepareGitHubInstallationSource } from '@/lib/knowledge/application/github-installation-source' const installed = { @@ -32,6 +31,8 @@ const installed = { providerTenantId: '7', } const input = { + principal: { kind: 'session' as const, userId: 'admin', sessionId: 'session' }, + requestId: 'request', connectorType: 'github', credentialId: installed.id, organizationId: 'org', @@ -43,8 +44,7 @@ const input = { beforeEach(() => { vi.clearAllMocks() - m.access.mockResolvedValue({ credential: installed }) - m.canUse.mockReturnValue(true) + m.access.mockResolvedValue(installed) m.decrypt.mockResolvedValue({ decrypted: '{}' }) m.parse.mockReturnValue({ installationId: '42', accountId: '7' }) m.repository.mockResolvedValue({ id: '123', fullName: 'example/private', defaultBranch: 'main' }) @@ -58,10 +58,16 @@ describe('GitHub installation source identity', () => { sourceConfig: { ...input.sourceConfig, githubRepositoryId: '999' }, }) ).resolves.toEqual({ repository: 'example/private', githubRepositoryId: '123' }) - expect(m.access).toHaveBeenCalledWith(installed.id, 'admin') + expect(m.access).toHaveBeenCalledWith( + expect.objectContaining({ + principal: input.principal, + credentialId: installed.id, + scope: { kind: 'organization', organizationId: 'org' }, + }) + ) }) it.each([ - { organizationId: undefined }, + { organizationId: undefined, workspaceId: 'workspace' }, { isSearchIndex: false }, { accessMode: 'admin' }, { accessMode: 'workspace' }, @@ -78,22 +84,20 @@ describe('GitHub installation source identity', () => { { revokedAt: new Date() }, { encryptedServiceAccountKey: null }, ])('refuses unusable or cross-scope installation credentials: %j', async (change) => { - m.access.mockResolvedValue({ credential: { ...installed, ...change } }) + m.access.mockResolvedValue({ ...installed, ...change }) await expect(prepareGitHubInstallationSource(input)).rejects.toMatchObject({ code: 'forbidden', }) expect(m.decrypt).not.toHaveBeenCalled() }) it('refuses credentials the acting user cannot use', async () => { - m.canUse.mockReturnValue(false) + m.access.mockRejectedValue(new OrchestrationError('forbidden', 'Credential access denied')) await expect(prepareGitHubInstallationSource(input)).rejects.toMatchObject({ code: 'forbidden', }) }) it('bounds encrypted binding data before decryption', async () => { - m.access.mockResolvedValue({ - credential: { ...installed, encryptedServiceAccountKey: 'x'.repeat(16_385) }, - }) + m.access.mockResolvedValue({ ...installed, encryptedServiceAccountKey: 'x'.repeat(16_385) }) await expect(prepareGitHubInstallationSource(input)).rejects.toMatchObject({ code: 'validation', }) @@ -122,7 +126,7 @@ describe('GitHub installation source identity', () => { prepareGitHubInstallationSource({ ...input, previousConfig: { githubRepositoryId: '123' } }) ).resolves.toMatchObject({ githubRepositoryId: '123' }) }) - it.each([null, { credential: { providerId: 'github-repositories' } }])( + it.each([null, { providerId: 'github-repositories' }])( 'cannot downgrade an existing installation by replacing or deleting its credential', async (access) => { m.access.mockResolvedValue(access) diff --git a/apps/sim/lib/knowledge/application/github-installation-source.ts b/apps/sim/lib/knowledge/application/github-installation-source.ts index c289b4accc4..8ee2daa57b4 100644 --- a/apps/sim/lib/knowledge/application/github-installation-source.ts +++ b/apps/sim/lib/knowledge/application/github-installation-source.ts @@ -1,6 +1,8 @@ +import type { Principal } from '@sim/auth/principal' import { OrchestrationError } from '@/lib/core/orchestration/types' +import { resourceScopeFromOwner } from '@/lib/core/resource-scope' import { decryptSecret } from '@/lib/core/security/encryption' -import { canUseCredential, getCredentialActorContext } from '@/lib/credentials/access' +import { requireConnectorCredential } from '@/lib/knowledge/application/connector-credential' import { parseGitHubInstallationBinding, resolveGitHubInstallationRepository, @@ -8,6 +10,9 @@ import { import { GITHUB_INSTALLATION_PROVIDER_ID } from '@/lib/oauth/github-installation-types' interface GitHubInstallationSourceInput { + principal: Principal + requestId: string + workspaceId?: string connectorType: string credentialId?: string | null organizationId?: string @@ -32,10 +37,13 @@ export async function prepareGitHubInstallationSource( ) return input.sourceConfig } - const access = input.credentialId - ? await getCredentialActorContext(input.credentialId, input.actingUserId) + const contentCredential = input.credentialId + ? await requireConnectorCredential({ + ...input, + credentialId: input.credentialId, + scope: resourceScopeFromOwner(input), + }) : null - const contentCredential = access?.credential if (contentCredential?.providerId !== GITHUB_INSTALLATION_PROVIDER_ID) { if (wasInstallation || assertedId !== undefined) throw new OrchestrationError( @@ -50,8 +58,6 @@ export async function prepareGitHubInstallationSource( 'GitHub installations require organization Search with connected member access' ) if ( - !access || - !canUseCredential(access) || contentCredential.organizationId !== input.organizationId || contentCredential.workspaceId !== null || contentCredential.type !== 'service_account' ||