Skip to content

Commit d537662

Browse files
feat(gitlab): support non-admin tokens with CSV permissions (#7752)
* feat(gitlab): support non-admin tokens with CSV permissions * fix(gitlab): address setup review and test expectations
1 parent 122ea0b commit d537662

54 files changed

Lines changed: 29186 additions & 187 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/content/docs/search/gitlab.mdx‎

Lines changed: 105 additions & 42 deletions
Large diffs are not rendered by default.

‎apps/docs/content/docs/search/index.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Some member sources offer **Sync documents with**, either directly in setup or u
6969
| --- | --- | --- |
7070
| [Confluence](/search/confluence) | Pages and blog posts | Service account or member accounts; each teammate connects |
7171
| [GitHub](/search/github) | Repository text files | App installation or member indexing; each teammate connects |
72-
| [GitLab](/search/gitlab) | Repository files, wikis, issues, merge requests | Self-managed instance administrator token; no member connection |
72+
| [GitLab](/search/gitlab) | Repository files, wikis, issues, merge requests | Self-managed administrator token, or non-admin token with CSV permissions; no member connection |
7373
| [Gmail](/search/gmail) | Email thread text | Delegated service account or member accounts |
7474
| [Google Calendar](/search/google-calendar) | Meetings | Delegated service account or member accounts |
7575
| [Google Drive](/search/google-drive) | Supported Drive files | Delegated service account or member accounts |
37.6 KB
Loading
37 KB
Loading

‎apps/sim/app/api/knowledge/[id]/connectors/route.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const POST = defineInternalJsonRoute({
4747
connectorType: body.connectorType,
4848
credentialId: body.credentialId,
4949
apiKey: body.apiKey,
50+
permissionConfig: body.permissionConfig,
5051
sourceConfig: body.sourceConfig,
5152
syncIntervalMinutes: body.syncIntervalMinutes,
5253
accessMode: body.accessMode,

‎apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
244244
: meta.auth.mode === 'oauth' &&
245245
meta.auth.adminCredentialType === 'service_account'
246246
? 'Service account'
247-
: 'Admin account',
247+
: 'Admin or service account',
248248
!approved
249249
? 'Deactivated'
250250
: !source.enabled

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ describe('Search setup options', () => {
794794
}
795795
)
796796

797-
it.each(SEARCH_SETUP_FIELDS)(
797+
it.each(SEARCH_SETUP_FIELDS.filter(({ connectorType }) => connectorType !== 'gitlab'))(
798798
'keeps $connectorType general knowledge-base fields and sync frequency outside document details',
799799
async ({ connectorType, primary, optional, cap }) => {
800800
await render({
@@ -814,6 +814,21 @@ describe('Search setup options', () => {
814814
}
815815
)
816816

817+
it('uses GitLab service-account access and token tabs without an access selector in regular KBs', async () => {
818+
await render({
819+
initialConnectorType: 'gitlab',
820+
initialAccessMode: 'workspace',
821+
isSearchIndex: false,
822+
})
823+
expect(document.body.textContent).toContain('Administrator token')
824+
expect(document.body.textContent).toContain('Non-admin token')
825+
expect(document.body.textContent).not.toContain('Connection method')
826+
expect(button('More options')).toHaveAttribute('aria-expanded', 'false')
827+
expect(document.body.textContent).not.toContain('Sync Frequency')
828+
await act(async () => button('More options').click())
829+
expect(document.body.textContent).toContain('Sync Frequency')
830+
})
831+
817832
it('keeps administrator-required fields in the primary form even if metadata marks them optional', async () => {
818833
mocks.credentials = [{ id: 'service', name: 'Indexing account', type: 'service_account' }]
819834
await render({ initialConnectorType: 'google_drive', initialAccessMode: 'admin' })

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx‎

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ArrowLeft, ChevronDown, ChevronRight, Plus, Search } from '@sim/emcn/ic
2121
import type { ConnectorData } from '@/lib/api/contracts/knowledge/connectors'
2222
import { type ResourceScope, resourceScopeFields } from '@/lib/core/resource-scope'
2323
import { getIntegrationsForCredentialProvider } from '@/lib/integrations/credential-display'
24+
import { initialConnectorAccessMode } from '@/lib/knowledge/connectors/access-modes'
2425
import {
2526
getCanonicalScopesForProvider,
2627
getProviderIdFromServiceId,
@@ -62,6 +63,11 @@ import {
6263
import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
6364
import { withBrandIcon } from '@/blocks/brand-icon'
6465
import { getConnectorApiKeyConfig, isConnectorCredentialTypeAllowed } from '@/connectors/auth'
66+
import {
67+
GitLabPermissionTabs,
68+
GitLabPermissionUploads,
69+
} from '@/connectors/gitlab/permission-config/fields'
70+
import { useGitLabPermissionForm } from '@/connectors/gitlab/permission-config/use-permission-form'
6571
import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
6672
import type { ConnectorConfigField, ConnectorMeta } from '@/connectors/types'
6773
import { useCreateConnector } from '@/hooks/queries/kb/connectors'
@@ -134,14 +140,16 @@ export function AddConnectorModal({
134140
draft?.contentCredentialId ?? null
135141
)
136142
const [access, setAccess] = useState<ConnectorAccessSelection>(() => ({
137-
accessMode:
143+
accessMode: initialConnectorAccessMode(
144+
initialType ? CONNECTOR_META_REGISTRY[initialType] : undefined,
138145
lockedAccessMode ??
139-
(lockConnectorType ? initialAccessMode : draft?.accessMode) ??
140-
(isSearchIndex && initialAccessMode === 'workspace'
141-
? initialType && CONNECTOR_META_REGISTRY[initialType]?.auth.mode === 'apiKey'
142-
? 'admin'
143-
: 'members'
144-
: initialAccessMode),
146+
(lockConnectorType ? initialAccessMode : draft?.accessMode) ??
147+
(isSearchIndex && initialAccessMode === 'workspace'
148+
? initialType && CONNECTOR_META_REGISTRY[initialType]?.auth.mode === 'apiKey'
149+
? 'admin'
150+
: 'members'
151+
: initialAccessMode)
152+
),
145153
}))
146154
const [disabledTagIds, setDisabledTagIds] = useState<Set<string>>(
147155
() => new Set(draft?.disabledTagIds)
@@ -154,6 +162,7 @@ export function AddConnectorModal({
154162
)
155163
const [showGitHubInstallationModal, setShowGitHubInstallationModal] = useState(false)
156164

165+
const gitlabPermissions = useGitLabPermissionForm()
157166
const [apiKeyValue, setApiKeyValue] = useState('')
158167
const [useApiKey, setUseApiKey] = useState(!isSearchIndex)
159168
const [apiKeyFocused, setApiKeyFocused] = useState(false)
@@ -182,6 +191,7 @@ export function AddConnectorModal({
182191
? getSearchConnectionLabels(selectedType, access.accessMode)
183192
: undefined
184193
const modalTitle = searchLabels?.title ?? `Configure ${connectorConfig?.name}`
194+
const showGitLabPermissions = selectedType === 'gitlab' && access.accessMode === 'admin'
185195
const isMembersMode = access.accessMode === 'members'
186196
const apiKeyConfig = connectorConfig ? getConnectorApiKeyConfig(connectorConfig.auth) : undefined
187197
const isApiKeyMode =
@@ -347,7 +357,7 @@ export function AddConnectorModal({
347357
}
348358

349359
const isOptionalSetupField = (field: ConnectorConfigField) =>
350-
isSearchIndex &&
360+
(isSearchIndex || connectorConfig?.supportedAccessModes?.length === 1) &&
351361
field.setupGroup === 'options' &&
352362
Boolean(connectorConfig && !isConnectorFieldRequired(field, connectorConfig, access.accessMode))
353363
const hasOptionalSetupFields = connectorConfig?.configFields.some(
@@ -433,24 +443,32 @@ export function AddConnectorModal({
433443

434444
const closeSetup = (nextOpen: boolean) => {
435445
if (!nextOpen && setupDraftKey) useConnectorSetupStore.getState().clearDraft(setupDraftKey)
446+
if (!nextOpen) {
447+
gitlabPermissions.reset()
448+
setApiKeyValue('')
449+
}
436450
onOpenChange(nextOpen)
437451
}
438452

439453
const handleSelectType = (type: string) => {
440454
if (setupDraftKey) useConnectorSetupStore.getState().clearDraft(setupDraftKey)
455+
gitlabPermissions.reset()
441456
setSelectedType(type)
442457
setSourceConfig(
443458
isSearchIndex ? { ...CONNECTOR_META_REGISTRY[type]?.searchDefaultSourceConfig } : {}
444459
)
445460
setSelectedCredentialId(null)
446461
setContentCredentialId(null)
447-
setAccess(
448-
isSearchIndex
449-
? {
450-
accessMode: CONNECTOR_META_REGISTRY[type]?.auth.mode === 'apiKey' ? 'admin' : 'members',
451-
}
452-
: WORKSPACE_ACCESS
453-
)
462+
setAccess({
463+
accessMode: initialConnectorAccessMode(
464+
CONNECTOR_META_REGISTRY[type],
465+
isSearchIndex
466+
? CONNECTOR_META_REGISTRY[type]?.auth.mode === 'apiKey'
467+
? 'admin'
468+
: 'members'
469+
: 'workspace'
470+
),
471+
})
454472
setApiKeyValue('')
455473
setUseApiKey(!isSearchIndex)
456474
setApiKeyFocused(false)
@@ -477,6 +495,7 @@ export function AddConnectorModal({
477495
const canSubmit = Boolean(
478496
connectorConfig &&
479497
hasRequiredCredential &&
498+
(!showGitLabPermissions || gitlabPermissions.complete) &&
480499
hasSearchAccess &&
481500
(access.accessMode !== 'admin' || allowAdmin) &&
482501
(!isMembersMode || allowMembers) &&
@@ -518,6 +537,7 @@ export function AddConnectorModal({
518537
{
519538
knowledgeBaseId,
520539
connectorType: selectedType,
540+
...(showGitLabPermissions ? { permissionConfig: gitlabPermissions.input } : {}),
521541
accessMode: access.accessMode,
522542
...(isApiKeyMode
523543
? apiKeyValue.trim()
@@ -625,6 +645,9 @@ export function AddConnectorModal({
625645
</div>
626646
) : connectorConfig ? (
627647
<>
648+
{showGitLabPermissions && (
649+
<GitLabPermissionTabs form={gitlabPermissions} disabled={isCreating} />
650+
)}
628651
{integrationAvailabilityError && (
629652
<ChipModalField type='custom' title='Connection availability'>
630653
<SettingsQueryErrorState
@@ -774,6 +797,9 @@ export function AddConnectorModal({
774797
}
775798
/>
776799
)}
800+
{showGitLabPermissions && (
801+
<GitLabPermissionUploads form={gitlabPermissions} disabled={isCreating} />
802+
)}
777803

778804
{(hasOptionalSetupFields ||
779805
contentCredentialField ||
@@ -786,7 +812,9 @@ export function AddConnectorModal({
786812
aria-expanded={showMetadata}
787813
onClick={() => setShowMetadata((visible) => !visible)}
788814
>
789-
{isSearchIndex ? 'More options' : 'Document details (optional)'}
815+
{isSearchIndex || hasOptionalSetupFields
816+
? 'More options'
817+
: 'Document details (optional)'}
790818
</Chip>
791819
</div>
792820
{showMetadata && (
@@ -841,15 +869,19 @@ export function AddConnectorModal({
841869
</>
842870
)}
843871

844-
{!isSearchIndex && (
872+
{!isSearchIndex && (!hasOptionalSetupFields || showMetadata) && (
845873
<ChipModalField
846874
type='custom'
847875
title='Sync Frequency'
848-
hint={connectorSyncFrequencyHint(
849-
access.accessMode,
850-
syncInterval,
851-
Boolean(contentCredentialId)
852-
)}
876+
hint={
877+
showGitLabPermissions && gitlabPermissions.mode === 'csv'
878+
? undefined
879+
: connectorSyncFrequencyHint(
880+
access.accessMode,
881+
syncInterval,
882+
Boolean(contentCredentialId)
883+
)
884+
}
853885
>
854886
<ButtonGroup
855887
value={String(syncInterval)}

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,20 @@ describe('connection method selection', () => {
155155
expect(onChange).toHaveBeenCalledWith({ accessMode: 'admin' })
156156
})
157157

158-
it('summarizes a single supported method without a selector', async () => {
158+
it('omits the field when the connector supports only the selected method', async () => {
159159
await render({ connectorConfig: gitlabConnectorMeta, value: { accessMode: 'admin' } })
160160
expect(container.querySelector('[role="radiogroup"]')).toBeNull()
161-
expect(container.textContent).toContain('Admin or service account')
161+
expect(container.textContent).toBe('')
162+
})
163+
164+
it('keeps pending upgrade actions without restoring the redundant selector', async () => {
165+
await render({
166+
connectorConfig: gitlabConnectorMeta,
167+
value: { accessMode: 'admin' },
168+
footer: <button type='button'>Apply changes</button>,
169+
})
170+
expect(container.querySelector('[role="radiogroup"]')).toBeNull()
171+
expect(container.querySelector('button')?.textContent).toBe('Apply changes')
162172
})
163173

164174
it('shows ordinary members a summary without editable or disabled choices', async () => {

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@sim/emcn'
1313
import type { ConnectorAccessMode } from '@/lib/api/contracts/knowledge/connectors'
1414
import { type ResourceScope, resourceScopeFromOwner } from '@/lib/core/resource-scope'
15+
import { supportsConnectorAccessMode } from '@/lib/knowledge/connectors/access-modes'
1516
import { slackSearchSetupHref } from '@/lib/sim-search/setup-navigation'
1617
import { connectorMemberProvider } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access'
1718
import {
@@ -135,6 +136,13 @@ export function ConnectorAccessField({
135136
allowed: adminSupported && allowAdmin,
136137
},
137138
]
139+
for (const entry of modes)
140+
entry.allowed &&= supportsConnectorAccessMode(connectorConfig, entry.mode)
141+
if (
142+
connectorConfig.supportedAccessModes?.length === 1 &&
143+
modes.some((entry) => entry.mode === value.accessMode && entry.allowed)
144+
)
145+
return canAdmin && footer ? <div className='px-2'>{footer}</div> : null
138146
/** Keep a retired current method visible so an admin can select an available replacement. */
139147
const visibleModes = modes.filter((entry) => entry.allowed || entry.mode === value.accessMode)
140148
const currentMode = modes.find((entry) => entry.mode === value.accessMode)

0 commit comments

Comments
 (0)