@@ -6,7 +6,10 @@ import {
66 isBlockTypeAccessControlExempt ,
77 resolveAccessControlBlockType ,
88} from '@/lib/permission-groups/block-access'
9- import type { SelectorCredentialPolicy } from '@/lib/selectors/server/types'
9+ import type {
10+ SelectorCredentialPolicy ,
11+ ServerSelectorAttachment ,
12+ } from '@/lib/selectors/server/types'
1013import { IntegrationNotAllowedError } from '@/ee/access-control/utils/permission-check'
1114
1215const logger = createLogger ( 'SelectorIntegrationAccess' )
@@ -32,6 +35,34 @@ export function selectorResourceServiceIds(policy: SelectorCredentialPolicy): re
3235 return policy . resourceServiceId ? [ policy . resourceServiceId ] : policy . serviceIds
3336}
3437
38+ /**
39+ * The block types an allowlist decision about this selector is made against.
40+ *
41+ * Two independent sources, because the OAuth credential catalog cannot identify
42+ * every selector that reaches a third-party API. A selector authenticated from
43+ * raw context fields (CloudWatch's AWS keys, IMAP's host and password) carries
44+ * no credential policy, and an API-key integration (Snowflake, NetSuite,
45+ * Harmonic) owns no OAuth catalog entry, so its service id maps to no block
46+ * type — both used to yield an empty list and pass the gate untested. They
47+ * declare `integrationBlockTypes` instead, and it wins over the catalog when
48+ * both are present.
49+ *
50+ * An empty result means "no integration identity", which is a pass. That is
51+ * reserved for the internal selectors — workspace files, knowledge bases,
52+ * tables — which read only Sim's own data;
53+ * `selectorIntegrationCoverage` in the manifest test keeps every
54+ * `provider-server` selector out of it.
55+ */
56+ export function selectorIntegrationBlockTypes (
57+ attachment : Pick < ServerSelectorAttachment , 'credential' | 'integrationBlockTypes' >
58+ ) : readonly string [ ] {
59+ if ( attachment . integrationBlockTypes ?. length ) return attachment . integrationBlockTypes
60+ if ( ! attachment . credential ) return [ ]
61+ return selectorResourceServiceIds ( attachment . credential ) . flatMap ( ( serviceId ) =>
62+ getIntegrationTypesForOAuthServiceId ( serviceId )
63+ )
64+ }
65+
3566/**
3667 * Refuses a selector execution whose integration the caller's permission group
3768 * does not permit.
@@ -54,13 +85,12 @@ export function selectorResourceServiceIds(policy: SelectorCredentialPolicy): re
5485 * bound to `slack_v2` match.
5586 *
5687 * A `null` allowlist, a caller no group governs, and a selector with no
57- * integration identity all pass through. The last covers two real shapes: an
58- * internal selector (workspace files, knowledge bases) declares no credential
59- * policy at all, and an API-key integration — Snowflake, NetSuite, Harmonic —
60- * owns no OAuth entry in the deployment integration catalog and therefore maps
61- * to no block type. Treating an unmapped service as allowed is deliberate and
62- * is what the credential catalog already does; see
63- * `isOAuthServiceAllowedByIntegrationTypes`.
88+ * integration identity all pass through. The last is now reserved for the
89+ * internal selectors — workspace files, knowledge bases, tables — which read
90+ * only Sim's own data and are not an integration at all. Every selector that
91+ * reaches a third party has an identity, either through the OAuth credential
92+ * catalog or through the `integrationBlockTypes` a raw-context or API-key
93+ * selector declares; see {@link selectorIntegrationBlockTypes}.
6494 *
6595 * One service can still map to several block types — the `google-drive` entry
6696 * authenticates both `google_drive` and `google_slides_v2` — and any of them
@@ -71,18 +101,14 @@ export function selectorResourceServiceIds(policy: SelectorCredentialPolicy): re
71101export async function assertSelectorIntegrationAllowed ( input : {
72102 principal : Principal
73103 workspaceId : string
74- serviceIds : readonly string [ ]
104+ blockTypes : readonly string [ ]
75105} ) : Promise < void > {
76- if ( input . serviceIds . length === 0 ) return
106+ const blockTypes = input . blockTypes
107+ if ( blockTypes . length === 0 ) return
77108
78109 const allowlist = await allowedIntegrationTypes ( input . principal , input . workspaceId )
79110 if ( allowlist === null ) return
80111
81- const blockTypes = input . serviceIds . flatMap ( ( serviceId ) =>
82- getIntegrationTypesForOAuthServiceId ( serviceId )
83- )
84- if ( blockTypes . length === 0 ) return
85-
86112 const allowed = blockTypes . some (
87113 ( blockType ) =>
88114 isBlockTypeAccessControlExempt ( blockType ) ||
0 commit comments