From 15b87ad08286b698fb3c6937658e38780aca7d9d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 10 Nov 2026 11:04:09 -0800 Subject: [PATCH 1/2] feat(search): manage source permissions through a shared API and CLI --- apps/docs/content/docs/cli/organizations.mdx | 25 ++- apps/docs/content/docs/cli/reference.mdx | 131 ++++++------ apps/docs/content/docs/search/gitlab.mdx | 9 +- apps/docs/openapi-v2-resources.json | 187 ++++++++++-------- .../scripts/fixtures/content-baseline.json | 40 ++-- .../route.ts | 24 +-- .../gitlab/permission-config/capability.ts | 28 ++- .../gitlab/permission-config/repository.ts | 15 +- .../knowledge/connector-permissions.ts | 7 + .../contracts/knowledge/gitlab-permissions.ts | 2 +- .../api/contracts/v2/openapi/organizations.ts | 81 ++++---- .../sim/lib/api/contracts/v2/organizations.ts | 54 ++--- .../lib/api/mcp/generated/v2-operations.ts | 36 ++-- .../server/routes/copilot-refused-routes.json | 8 +- .../server/routes/v2-route-table.generated.ts | 4 +- .../lib/knowledge/application/operations.ts | 6 +- ...h-permissions.ts => source-permissions.ts} | 93 ++++----- .../knowledge/connectors/permission-config.ts | 13 ++ .../knowledge/connectors/permission-store.ts | 14 +- .../test-gitlab-csv-permissions-e2e.ts | 51 +++-- packages/sim-cli/src/contract/commands.ts | 10 +- packages/sim-cli/src/generated/v2-api.ts | 106 +++++----- packages/sim-cli/src/http/client.test.ts | 2 +- scripts/check-unused-exports.baseline.json | 1 - scripts/generate-cli-docs.ts | 2 +- 25 files changed, 522 insertions(+), 427 deletions(-) rename apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/{gitlab-permissions => permissions}/route.ts (55%) rename apps/sim/lib/knowledge/application/{gitlab-search-permissions.ts => source-permissions.ts} (57%) diff --git a/apps/docs/content/docs/cli/organizations.mdx b/apps/docs/content/docs/cli/organizations.mdx index 949884d5180..99374c76ef6 100644 --- a/apps/docs/content/docs/cli/organizations.mdx +++ b/apps/docs/content/docs/cli/organizations.mdx @@ -229,14 +229,14 @@ sim organizations members remove [options] API request and response schemas: [removeOrganizationMember](/api-reference/organizations/removeOrganizationMember). -## GitLab Search permissions +## Search source permissions -### Get GitLab search permission revision +### Get search source permission revision OAuth login or personal API key required. ```bash -sim organizations gitlab-permissions get [options] +sim organizations search sources permissions get [options] ``` **Arguments** @@ -245,7 +245,7 @@ sim organizations gitlab-permissions get [options] | Argument | Required | Description | | --- | --- | --- | -| `sourceId` | Yes | Identifier of the configured GitLab Search source. | +| `sourceId` | Yes | Identifier of the configured Search source. | @@ -259,16 +259,16 @@ sim organizations gitlab-permissions get [options] -API request and response schemas: [getGitLabSearchPermissions](/api-reference/organizations/getGitLabSearchPermissions). +API request and response schemas: [getSearchSourcePermissions](/api-reference/organizations/getSearchSourcePermissions). -### Replace GitLab search permission CSVs +### Replace search source permissions OAuth login or personal API key required. -This replaces both GitLab permission files. Readers omitted from the new grants lose access. +This replaces the source’s permission grants. Readers omitted from the new grants lose access. ```bash -sim organizations gitlab-permissions replace [options] +sim organizations search sources permissions replace [options] ``` **Arguments** @@ -277,7 +277,7 @@ sim organizations gitlab-permissions replace [options] | Argument | Required | Description | | --- | --- | --- | -| `sourceId` | Yes | Identifier of the configured GitLab Search source. | +| `sourceId` | Yes | Identifier of the configured Search source. | @@ -288,14 +288,13 @@ sim organizations gitlab-permissions replace [options] | Option | Required | Description | | --- | --- | --- | | `--organization ` | Yes | Organization identifier. | -| `--expected-revision ` | Yes | Current permission revision obtained before generating this snapshot. | -| `--user-mapping ` | Yes | GitLab user IDs and their verified Sim email addresses. (JSON, or @path / @- to read a file or stdin). | -| `--project-permissions ` | Yes | GitLab project paths and the IDs of users granted access. (JSON, or @path / @- to read a file or stdin). | +| `--expected-revision ` | Yes | Permission revision read before exporting the grants. | +| `--permissions ` | Yes | Complete permission grants in the source provider’s format. (JSON, or @path / @- to read a file or stdin). | | `-y, --yes` | Yes | Confirm this operation. | -API request and response schemas: [replaceGitLabSearchPermissions](/api-reference/organizations/replaceGitLabSearchPermissions). +API request and response schemas: [replaceSearchSourcePermissions](/api-reference/organizations/replaceSearchSourcePermissions). ## Invitations diff --git a/apps/docs/content/docs/cli/reference.mdx b/apps/docs/content/docs/cli/reference.mdx index 1b1a2bc7090..ff141b26964 100644 --- a/apps/docs/content/docs/cli/reference.mdx +++ b/apps/docs/content/docs/cli/reference.mdx @@ -4288,72 +4288,6 @@ sim organizations sso policy update [options] API request and response schemas: [updateSsoPolicy](/api-reference/organizations/updateSsoPolicy). -### sim organizations gitlab-permissions get - -Get GitLab search permission revision (OAuth login or personal API key required) - -```bash -sim organizations gitlab-permissions get [options] -``` - -**Arguments** - - - -| Argument | Required | Description | -| --- | --- | --- | -| `sourceId` | Yes | Identifier of the configured GitLab Search source. | - - - -**Options** - - - -| Option | Required | Description | -| --- | --- | --- | -| `--organization ` | Yes | Organization identifier. | - - - -API request and response schemas: [getGitLabSearchPermissions](/api-reference/organizations/getGitLabSearchPermissions). - -### sim organizations gitlab-permissions replace - -Replace GitLab search permission CSVs (OAuth login or personal API key required) - -This replaces both GitLab permission files. Readers omitted from the new grants lose access. - -```bash -sim organizations gitlab-permissions replace [options] -``` - -**Arguments** - - - -| Argument | Required | Description | -| --- | --- | --- | -| `sourceId` | Yes | Identifier of the configured GitLab Search source. | - - - -**Options** - - - -| Option | Required | Description | -| --- | --- | --- | -| `--organization ` | Yes | Organization identifier. | -| `--expected-revision ` | Yes | Current permission revision obtained before generating this snapshot. | -| `--user-mapping ` | Yes | GitLab user IDs and their verified Sim email addresses. (JSON, or @path / @- to read a file or stdin). | -| `--project-permissions ` | Yes | GitLab project paths and the IDs of users granted access. (JSON, or @path / @- to read a file or stdin). | -| `-y, --yes` | Yes | Confirm this operation. | - - - -API request and response schemas: [replaceGitLabSearchPermissions](/api-reference/organizations/replaceGitLabSearchPermissions). - ### sim organizations get Get organization (OAuth login or personal API key required) @@ -4602,6 +4536,71 @@ sim organizations usage events [options] API request and response schemas: [listOrganizationUsageEvents](/api-reference/organizations/listOrganizationUsageEvents). +### sim organizations search sources permissions get + +Get search source permission revision (OAuth login or personal API key required) + +```bash +sim organizations search sources permissions get [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `sourceId` | Yes | Identifier of the configured Search source. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--organization ` | Yes | Organization identifier. | + + + +API request and response schemas: [getSearchSourcePermissions](/api-reference/organizations/getSearchSourcePermissions). + +### sim organizations search sources permissions replace + +Replace search source permissions (OAuth login or personal API key required) + +This replaces the source’s permission grants. Readers omitted from the new grants lose access. + +```bash +sim organizations search sources permissions replace [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `sourceId` | Yes | Identifier of the configured Search source. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--organization ` | Yes | Organization identifier. | +| `--expected-revision ` | Yes | Permission revision read before exporting the grants. | +| `--permissions ` | Yes | Complete permission grants in the source provider’s format. (JSON, or @path / @- to read a file or stdin). | +| `-y, --yes` | Yes | Confirm this operation. | + + + +API request and response schemas: [replaceSearchSourcePermissions](/api-reference/organizations/replaceSearchSourcePermissions). + ### sim organizations list List organizations (OAuth login or personal API key required) diff --git a/apps/docs/content/docs/search/gitlab.mdx b/apps/docs/content/docs/search/gitlab.mdx index 3317022616a..92ccdbf49fd 100644 --- a/apps/docs/content/docs/search/gitlab.mdx +++ b/apps/docs/content/docs/search/gitlab.mdx @@ -89,7 +89,7 @@ To rotate the PAT, supply its replacement in source settings. A blank token reta ### Automate updates with GitLab CI -After an admin configures a source in **Non-admin token** mode, a job can [read its revision](/api-reference/organizations/getGitLabSearchPermissions) and [replace both CSVs](/api-reference/organizations/replaceGitLabSearchPermissions). Copy `SIM_ORG_ID` and `SIM_SOURCE_ID` from the source settings URL: `/o//settings/integrations/sources/`. Set `SIM_BASE_URL` to your Sim origin (for hosted Sim, `https://www.sim.ai`). +After an admin configures a source in **Non-admin token** mode, a job can [read its revision](/api-reference/organizations/getSearchSourcePermissions) and [replace both CSVs](/api-reference/organizations/replaceSearchSourcePermissions). Copy `SIM_ORG_ID` and `SIM_SOURCE_ID` from the source settings URL: `/o//settings/integrations/sources/`. Set `SIM_BASE_URL` to your Sim origin (for hosted Sim, `https://www.sim.ai`). Store an organization admin's [personal Sim API key](/api-reference/authentication) as a [masked, protected GitLab CI variable](https://docs.gitlab.com/ci/variables/) named `SIM_API_KEY`. A personal key inherits its owner's access. This endpoint can only read the revision or replace both CSVs; it cannot change the GitLab PAT, host, or project. @@ -106,15 +106,16 @@ sync_gitlab_permissions: script: - | set -eu - url="${SIM_BASE_URL%/}/api/v2/organizations/$SIM_ORG_ID/search/sources/$SIM_SOURCE_ID/gitlab-permissions" + url="${SIM_BASE_URL%/}/api/v2/organizations/$SIM_ORG_ID/search/sources/$SIM_SOURCE_ID/permissions" auth="X-API-Key: $SIM_API_KEY" current=$(curl --fail --silent --show-error -H "$auth" "$url") revision=$(printf '%s' "$current" | jq -er '.data.revision') ./ci/export-gitlab-permissions jq -n --argjson rev "$revision" \ --rawfile users users.csv --rawfile grants project-permissions.csv \ - '{expectedRevision: $rev, userMapping: {filename: "users.csv", content: $users}, - projectPermissions: {filename: "project-permissions.csv", content: $grants}}' > update.json + '{expectedRevision: $rev, permissions: {provider: "gitlab", + userMapping: {filename: "users.csv", content: $users}, + projectPermissions: {filename: "project-permissions.csv", content: $grants}}}' > update.json curl --fail --silent --show-error --request PUT -H "$auth" \ -H 'Content-Type: application/json' --data-binary @update.json "$url" ``` diff --git a/apps/docs/openapi-v2-resources.json b/apps/docs/openapi-v2-resources.json index c01d2fa7365..89d0ab247db 100644 --- a/apps/docs/openapi-v2-resources.json +++ b/apps/docs/openapi-v2-resources.json @@ -9643,12 +9643,12 @@ } } }, - "/api/v2/organizations/{organizationId}/search/sources/{sourceId}/gitlab-permissions": { + "/api/v2/organizations/{organizationId}/search/sources/{sourceId}/permissions": { "get": { - "operationId": "getGitLabSearchPermissions", - "summary": "Get GitLab Search permission revision", - "description": "Read the current revision of an existing GitLab Search source in CSV permission mode. Requires organization Admin access. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.", - "x-sim-operation": "knowledge.search.gitlab_permissions.read", + "operationId": "getSearchSourcePermissions", + "summary": "Get Search source permission revision", + "description": "Read the permission revision for a Search source that accepts externally managed grants. Requires organization Admin access. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.", + "x-sim-operation": "knowledge.search.source_permissions.read", "x-oauth-scope": "api:read", "tags": ["Organizations"], "security": [ @@ -9675,12 +9675,12 @@ "name": "sourceId", "in": "path", "required": true, - "description": "Identifier of the configured GitLab Search source.", + "description": "Identifier of the configured Search source.", "schema": { "type": "string", "minLength": 1, "maxLength": 255, - "description": "Identifier of the configured GitLab Search source." + "description": "Identifier of the configured Search source." } } ], @@ -9701,7 +9701,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GitLabSearchPermissionResponse" + "$ref": "#/components/schemas/SearchSourcePermissionResponse" }, "examples": { "example1": { @@ -9740,10 +9740,10 @@ } }, "put": { - "operationId": "replaceGitLabSearchPermissions", - "summary": "Replace GitLab Search permission CSVs", - "description": "Replace both permission CSVs for an existing GitLab Search source in CSV mode. Requires organization Admin access. Supply the current revision. A concurrent change returns 409 and leaves both files untouched. Each file accepts up to 4 MiB and 100,000 data rows. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.", - "x-sim-operation": "knowledge.search.gitlab_permissions.replace", + "operationId": "replaceSearchSourcePermissions", + "summary": "Replace Search source permissions", + "description": "Replace the complete permission grants for one Search source. Requires organization Admin access. Supply the current revision and the grants for the configured provider. A stale revision returns 409. GitLab CSV mode accepts both files together, up to 4 MiB and 100,000 data rows each. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.", + "x-sim-operation": "knowledge.search.source_permissions.replace", "x-oauth-scope": "api:write", "tags": ["Organizations"], "security": [ @@ -9770,35 +9770,38 @@ "name": "sourceId", "in": "path", "required": true, - "description": "Identifier of the configured GitLab Search source.", + "description": "Identifier of the configured Search source.", "schema": { "type": "string", "minLength": 1, "maxLength": 255, - "description": "Identifier of the configured GitLab Search source." + "description": "Identifier of the configured Search source." } } ], "requestBody": { "required": true, - "description": "The expected revision and complete contents of both UTF-8 CSV files.", + "description": "The expected revision and complete replacement grants for the provider.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReplaceGitLabSearchPermissionsBody" + "$ref": "#/components/schemas/ReplaceSearchSourcePermissionsBody" }, "examples": { "example1": { "summary": "Request example 1", "value": { "expectedRevision": 2, - "userMapping": { - "filename": "users.csv", - "content": "user_id,email\n42,reader@example.com\n" - }, - "projectPermissions": { - "filename": "projects.csv", - "content": "project_path,user_id\nteam/project,42\n" + "permissions": { + "provider": "gitlab", + "userMapping": { + "filename": "users.csv", + "content": "user_id,email\n42,reader@example.com\n" + }, + "projectPermissions": { + "filename": "projects.csv", + "content": "project_path,user_id\nteam/project,42\n" + } } } } @@ -9823,7 +9826,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GitLabSearchPermissionResponse" + "$ref": "#/components/schemas/SearchSourcePermissionResponse" }, "examples": { "example1": { @@ -23526,33 +23529,33 @@ } ] }, - "GitLabSearchPermissionRevision": { + "SearchSourcePermissionRevision": { "type": "object", "properties": { "revision": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Current revision of the stored permissions." + "description": "Current permission revision." } }, "required": ["revision"], "additionalProperties": false, - "title": "GitLab Search permission revision", - "description": "Revision of the permission CSVs for this source." + "title": "Search source permission revision", + "description": "Revision of this source’s permission grants." }, - "GitLabSearchPermissionResponse": { + "SearchSourcePermissionResponse": { "type": "object", "properties": { "data": { "description": "Response data.", - "$ref": "#/components/schemas/GitLabSearchPermissionRevision" + "$ref": "#/components/schemas/SearchSourcePermissionRevision" } }, "required": ["data"], "additionalProperties": false, - "title": "GitLab Search permission response", - "description": "Current revision of the permission CSVs.", + "title": "Search source permission response", + "description": "Current revision of the source permission grants.", "examples": [ { "data": { @@ -23561,70 +23564,90 @@ } ] }, - "ReplaceGitLabSearchPermissionsBody": { + "ReplaceSearchSourcePermissionsBody": { "type": "object", "properties": { "expectedRevision": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Current permission revision obtained before generating this snapshot." - }, - "userMapping": { - "type": "object", - "properties": { - "filename": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "CSV filename without path separators." - }, - "content": { - "type": "string", - "minLength": 1, - "maxLength": 4194304, - "description": "UTF-8 CSV contents, up to 4 MiB." - } - }, - "required": ["filename", "content"], - "additionalProperties": false, - "description": "GitLab user IDs and their verified Sim email addresses." + "description": "Permission revision read before exporting the grants." }, - "projectPermissions": { - "type": "object", - "properties": { - "filename": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "CSV filename without path separators." - }, - "content": { - "type": "string", - "minLength": 1, - "maxLength": 4194304, - "description": "UTF-8 CSV contents, up to 4 MiB." + "permissions": { + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "gitlab", + "description": "Provider of the configured Search source." + }, + "userMapping": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "CSV filename without path separators." + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 4194304, + "description": "UTF-8 CSV contents, up to 4 MiB." + } + }, + "required": ["filename", "content"], + "additionalProperties": false, + "description": "GitLab user IDs and their verified Sim email addresses." + }, + "projectPermissions": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "CSV filename without path separators." + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 4194304, + "description": "UTF-8 CSV contents, up to 4 MiB." + } + }, + "required": ["filename", "content"], + "additionalProperties": false, + "description": "GitLab project paths and the IDs of users granted access." + } + }, + "required": ["provider", "userMapping", "projectPermissions"], + "additionalProperties": false } - }, - "required": ["filename", "content"], - "additionalProperties": false, - "description": "GitLab project paths and the IDs of users granted access." + ], + "description": "Complete permission grants in the source provider’s format." } }, - "required": ["expectedRevision", "userMapping", "projectPermissions"], + "required": ["expectedRevision", "permissions"], "additionalProperties": false, - "title": "GitLab Search permission CSVs", - "description": "The expected revision and complete contents of both UTF-8 CSV files.", + "title": "Search source permission grants", + "description": "The expected revision and complete replacement grants for the provider.", "examples": [ { "expectedRevision": 2, - "userMapping": { - "filename": "users.csv", - "content": "user_id,email\n42,reader@example.com\n" - }, - "projectPermissions": { - "filename": "projects.csv", - "content": "project_path,user_id\nteam/project,42\n" + "permissions": { + "provider": "gitlab", + "userMapping": { + "filename": "users.csv", + "content": "user_id,email\n42,reader@example.com\n" + }, + "projectPermissions": { + "filename": "projects.csv", + "content": "project_path,user_id\nteam/project,42\n" + } } } ] diff --git a/apps/docs/scripts/fixtures/content-baseline.json b/apps/docs/scripts/fixtures/content-baseline.json index 9b596d5ae9b..9506dc1385c 100644 --- a/apps/docs/scripts/fixtures/content-baseline.json +++ b/apps/docs/scripts/fixtures/content-baseline.json @@ -6749,15 +6749,15 @@ { "lang": "bash", "meta": null, - "sha256": "f57b9272801ee426bbcac1ba0a76951bd50f0d66de23916fe7beffd36a8078d7", - "bytes": 61, + "sha256": "229448ff3c47ffdfdd720fb88b38edcf58f51d063f7b7836d4f4fdc478f99b5c", + "bytes": 69, "line": 238 }, { "lang": "bash", "meta": null, - "sha256": "d28646b150b37f8c75a2ae8ab0ca07b698279c3b044530810a2f9096079d5ff9", - "bytes": 65, + "sha256": "7863a0cbd4e2cb3715358cca55047765a210f9a42a3dd171e42f574185fe0ab9", + "bytes": 73, "line": 270 }, { @@ -12609,20 +12609,6 @@ "bytes": 45, "line": 4259 }, - { - "lang": "bash", - "meta": null, - "sha256": "f57b9272801ee426bbcac1ba0a76951bd50f0d66de23916fe7beffd36a8078d7", - "bytes": 61, - "line": 4295 - }, - { - "lang": "bash", - "meta": null, - "sha256": "d28646b150b37f8c75a2ae8ab0ca07b698279c3b044530810a2f9096079d5ff9", - "bytes": 65, - "line": 4327 - }, { "lang": "bash", "meta": null, @@ -12686,6 +12672,20 @@ "bytes": 40, "line": 4499 }, + { + "lang": "bash", + "meta": null, + "sha256": "229448ff3c47ffdfdd720fb88b38edcf58f51d063f7b7836d4f4fdc478f99b5c", + "bytes": 69, + "line": 4543 + }, + { + "lang": "bash", + "meta": null, + "sha256": "7863a0cbd4e2cb3715358cca55047765a210f9a42a3dd171e42f574185fe0ab9", + "bytes": 73, + "line": 4575 + }, { "lang": "bash", "meta": null, @@ -177635,8 +177635,8 @@ { "lang": "yaml", "meta": "title=\".gitlab-ci.yml\"", - "sha256": "cbd29bdea80fc9e1c908abaa8e708dd21435267b66be0583a033686a199b8882", - "bytes": 1046, + "sha256": "bf3d6e7bec6b87f3966580713633eb8baed778c9dced4569a65cc62d790dee42", + "bytes": 1084, "line": 98 } ], diff --git a/apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions/route.ts b/apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route.ts similarity index 55% rename from apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions/route.ts rename to apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route.ts index 02ff63f5f57..392fad7ba08 100644 --- a/apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions/route.ts +++ b/apps/sim/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route.ts @@ -1,33 +1,33 @@ import { - v2GetGitLabSearchPermissionsContract, - v2ReplaceGitLabSearchPermissionsContract, + v2GetSearchSourcePermissionsContract, + v2ReplaceSearchSourcePermissionsContract, } from '@/lib/api/contracts/v2/organizations' import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' import { v2OrganizationErrorPolicy } from '@/lib/api/server/routes/organizations' +import { searchSourcePermissionOperations } from '@/lib/knowledge/application/operations' import { - readGitLabSearchPermissions, - replaceGitLabSearchPermissions, -} from '@/lib/knowledge/application/gitlab-search-permissions' -import { gitLabSearchPermissionOperations } from '@/lib/knowledge/application/operations' + readSearchSourcePermissions, + replaceSearchSourcePermissions, +} from '@/lib/knowledge/application/source-permissions' export const GET = defineV2JsonRoute({ - contract: v2GetGitLabSearchPermissionsContract, - operation: gitLabSearchPermissionOperations.read, + contract: v2GetSearchSourcePermissionsContract, + operation: searchSourcePermissionOperations.read, auth: v2ApiKeyAuth, rateLimit: v2RateLimits.publicApi, errorPolicy: v2OrganizationErrorPolicy, mapInput: ({ params }) => params, - useCase: readGitLabSearchPermissions, + useCase: readSearchSourcePermissions, present: (data) => ({ data }), }) export const PUT = defineV2JsonRoute({ - contract: v2ReplaceGitLabSearchPermissionsContract, - operation: gitLabSearchPermissionOperations.replace, + contract: v2ReplaceSearchSourcePermissionsContract, + operation: searchSourcePermissionOperations.replace, auth: v2ApiKeyAuth, rateLimit: v2RateLimits.publicApi, errorPolicy: v2OrganizationErrorPolicy, mapInput: ({ params, body }) => ({ ...params, ...body }), - useCase: replaceGitLabSearchPermissions, + useCase: replaceSearchSourcePermissions, present: (data) => ({ data }), }) diff --git a/apps/sim/connectors/gitlab/permission-config/capability.ts b/apps/sim/connectors/gitlab/permission-config/capability.ts index 8e469ea538e..9daf8b9f232 100644 --- a/apps/sim/connectors/gitlab/permission-config/capability.ts +++ b/apps/sim/connectors/gitlab/permission-config/capability.ts @@ -1,13 +1,32 @@ import type { ConnectorPermissionConfigCapability } from '@/lib/knowledge/connectors/permission-config' +import { writeConnectorPermissions } from '@/lib/knowledge/connectors/permission-store' import { prepareGitLabPermissionChange } from '@/connectors/gitlab/permission-config/prepare' import { + gitLabPermissionData, + prepareGitLabPermissions, + readGitLabPermissionMetadata, readGitLabPermissionSummaries, seedGitLabCsvContext, - writeGitLabPermissions, } from '@/connectors/gitlab/permission-config/repository' import { setGitLabCsvContext } from '@/connectors/gitlab/permission-config/types' export const gitLabPermissionConfig: ConnectorPermissionConfigCapability = { + grantReplacement: { + supports: (metadata) => readGitLabPermissionMetadata(metadata).mode === 'csv', + prepare(snapshot, expectedRevision, grants) { + const stored = { + ...readGitLabPermissionMetadata(snapshot.metadata), + revision: snapshot.revision, + } + const prepared = prepareGitLabPermissions( + { ...grants, mode: 'csv', expectedRevision }, + stored, + stored, + true + ) + return gitLabPermissionData(prepared) + }, + }, async prepare(input) { const change = await prepareGitLabPermissionChange({ ...input, @@ -24,7 +43,12 @@ export const gitLabPermissionConfig: ConnectorPermissionConfigCapability = { if (prepared.snapshot.mode === 'csv') setGitLabCsvContext(context, { connectorId, ...prepared.snapshot }) }, - write: (tx, connectorId) => writeGitLabPermissions(tx, connectorId, prepared), + async write(tx, connectorId) { + await writeConnectorPermissions(tx, connectorId, { + expectedRevision: prepared.expectedRevision, + ...gitLabPermissionData(prepared), + }) + }, } }, async readSummaries(connectorIds) { diff --git a/apps/sim/connectors/gitlab/permission-config/repository.ts b/apps/sim/connectors/gitlab/permission-config/repository.ts index 74f37054e56..65c3ffbdd9e 100644 --- a/apps/sim/connectors/gitlab/permission-config/repository.ts +++ b/apps/sim/connectors/gitlab/permission-config/repository.ts @@ -2,11 +2,10 @@ import { isPlainRecord } from '@sim/utils/object' import { z } from 'zod' import { gitLabPermissionSummarySchema } from '@/lib/api/contracts/knowledge/gitlab-permissions' import { OrchestrationError } from '@/lib/core/orchestration/types' -import type { DbTransaction } from '@/lib/db/types' import { + type ConnectorPermissionData, loadConnectorPermissionSnapshot, readConnectorPermissionMetadata, - writeConnectorPermissions, } from '@/lib/knowledge/connectors/permission-store' import { GitLabCsvValidationError, @@ -173,24 +172,18 @@ export function prepareGitLabPermissions( } } -/** The shared store serializes revision checks and grants inside the owning connector transaction. */ -export async function writeGitLabPermissions( - tx: DbTransaction, - connectorId: string, - prepared: PreparedGitLabPermissions -): Promise { +export function gitLabPermissionData(prepared: PreparedGitLabPermissions): ConnectorPermissionData { const { revision: _revision, userMappingRows, projectPermissionRows, ...metadata } = prepared.snapshot - await writeConnectorPermissions(tx, connectorId, { - expectedRevision: prepared.expectedRevision, + return { metadata, payload: { userMappingRows, projectPermissionRows }, groups: [{ groupKey: 'project', subjects: prepared.subjects }], - }) + } } /** Projects only safe metadata; CSV contents never reach a response or browser draft. */ diff --git a/apps/sim/lib/api/contracts/knowledge/connector-permissions.ts b/apps/sim/lib/api/contracts/knowledge/connector-permissions.ts index fd6fa65f17d..b81df83923a 100644 --- a/apps/sim/lib/api/contracts/knowledge/connector-permissions.ts +++ b/apps/sim/lib/api/contracts/knowledge/connector-permissions.ts @@ -7,6 +7,13 @@ import { export const connectorPermissionConfigSchema = z.discriminatedUnion('provider', [ gitLabPermissionInputSchema, ]) +export const connectorPermissionGrantsSchema = z.discriminatedUnion('provider', [ + gitLabPermissionInputSchema + .pick({ provider: true, userMapping: true, projectPermissions: true }) + .required(), +]) +export type ConnectorPermissionGrants = z.output + export const connectorPermissionSummarySchema = z.discriminatedUnion('provider', [ gitLabPermissionSummarySchema, ]) diff --git a/apps/sim/lib/api/contracts/knowledge/gitlab-permissions.ts b/apps/sim/lib/api/contracts/knowledge/gitlab-permissions.ts index 1c37329ef80..6c254e2b4d2 100644 --- a/apps/sim/lib/api/contracts/knowledge/gitlab-permissions.ts +++ b/apps/sim/lib/api/contracts/knowledge/gitlab-permissions.ts @@ -16,7 +16,7 @@ const gitLabCsvUploadSchema = z .strict() export const gitLabPermissionInputSchema = z .object({ - provider: z.literal('gitlab'), + provider: z.literal('gitlab').describe('Provider of the configured Search source.'), mode: z.enum(GITLAB_PERMISSION_MODES), expectedRevision: z .number() diff --git a/apps/sim/lib/api/contracts/v2/openapi/organizations.ts b/apps/sim/lib/api/contracts/v2/openapi/organizations.ts index 2c0a71310dd..17e406d5734 100644 --- a/apps/sim/lib/api/contracts/v2/openapi/organizations.ts +++ b/apps/sim/lib/api/contracts/v2/openapi/organizations.ts @@ -7,38 +7,38 @@ import { } from '@/lib/api/contracts/v2/openapi/shared' import { v2CreateOrganizationInvitationContract, - v2GetGitLabSearchPermissionsContract, v2GetOrganizationContract, v2GetOrganizationInvitationContract, + v2GetSearchSourcePermissionsContract, v2ListOrganizationInvitationsContract, v2ListOrganizationInvitationWorkspacesContract, v2ListOrganizationMembersContract, v2ListOrganizationsContract, v2ListOrganizationWorkspacesContract, v2RemoveOrganizationMemberContract, - v2ReplaceGitLabSearchPermissionsContract, + v2ReplaceSearchSourcePermissionsContract, v2ResendOrganizationInvitationContract, v2RevokeOrganizationInvitationContract, v2UpdateOrganizationMemberContract, } from '@/lib/api/contracts/v2/organizations' import { defineOpenApiRoute } from '@/lib/api/openapi/types' import { invitationOperations } from '@/lib/invitations/application/operations' -import { gitLabSearchPermissionOperations } from '@/lib/knowledge/application/operations' +import { searchSourcePermissionOperations } from '@/lib/knowledge/application/operations' import { organizationOperations } from '@/lib/organizations/application/operations' const TIMESTAMP = '2026-06-01T09:00:00.000Z' -const gitLabSearchPermissionParams = documentedSchema( - v2GetGitLabSearchPermissionsContract.params, - 'GitLabSearchPermissionParams', - 'GitLab Search permission parameters', +const searchSourcePermissionParams = documentedSchema( + v2GetSearchSourcePermissionsContract.params, + 'SearchSourcePermissionParams', + 'Search source permission parameters', 'Organization and Search source identifiers.' ) -const gitLabSearchPermissionResponse = documentedSchema( - v2GetGitLabSearchPermissionsContract.response.schema, - 'GitLabSearchPermissionResponse', - 'GitLab Search permission response', - 'Current revision of the permission CSVs.', +const searchSourcePermissionResponse = documentedSchema( + v2GetSearchSourcePermissionsContract.response.schema, + 'SearchSourcePermissionResponse', + 'Search source permission response', + 'Current revision of the source permission grants.', [{ data: { revision: 3 } }] ) @@ -570,56 +570,59 @@ export const organizationOpenApiRoutes = [ } ), defineOpenApiRoute( - v2GetGitLabSearchPermissionsContract, + v2GetSearchSourcePermissionsContract, { - applicationOperation: gitLabSearchPermissionOperations.read, - operationId: 'getGitLabSearchPermissions', - summary: 'Get GitLab Search permission revision', - description: `Read the current revision of an existing GitLab Search source in CSV permission mode. Requires organization Admin access. ${WORKSPACE_API_KEY_DENIED}`, + applicationOperation: searchSourcePermissionOperations.read, + operationId: 'getSearchSourcePermissions', + summary: 'Get Search source permission revision', + description: `Read the permission revision for a Search source that accepts externally managed grants. Requires organization Admin access. ${WORKSPACE_API_KEY_DENIED}`, tags: ['Organizations'], errors: RESOURCE_ERRORS, success: { description: 'The current permission revision.', headers: RATE_LIMIT_HEADERS }, }, { - params: gitLabSearchPermissionParams, - query: v2GetGitLabSearchPermissionsContract.query, - response: gitLabSearchPermissionResponse, + params: searchSourcePermissionParams, + query: v2GetSearchSourcePermissionsContract.query, + response: searchSourcePermissionResponse, } ), defineOpenApiRoute( - v2ReplaceGitLabSearchPermissionsContract, + v2ReplaceSearchSourcePermissionsContract, { - applicationOperation: gitLabSearchPermissionOperations.replace, - operationId: 'replaceGitLabSearchPermissions', - summary: 'Replace GitLab Search permission CSVs', - description: `Replace both permission CSVs for an existing GitLab Search source in CSV mode. Requires organization Admin access. Supply the current revision. A concurrent change returns 409 and leaves both files untouched. Each file accepts up to 4 MiB and 100,000 data rows. ${WORKSPACE_API_KEY_DENIED}`, + applicationOperation: searchSourcePermissionOperations.replace, + operationId: 'replaceSearchSourcePermissions', + summary: 'Replace Search source permissions', + description: `Replace the complete permission grants for one Search source. Requires organization Admin access. Supply the current revision and the grants for the configured provider. A stale revision returns 409. GitLab CSV mode accepts both files together, up to 4 MiB and 100,000 data rows each. ${WORKSPACE_API_KEY_DENIED}`, tags: ['Organizations'], errors: RESOURCE_CONFLICT_ERRORS, success: { description: 'The new permission revision.', headers: RATE_LIMIT_HEADERS }, }, { - params: gitLabSearchPermissionParams, - query: v2ReplaceGitLabSearchPermissionsContract.query, + params: searchSourcePermissionParams, + query: v2ReplaceSearchSourcePermissionsContract.query, body: documentedSchema( - v2ReplaceGitLabSearchPermissionsContract.body, - 'ReplaceGitLabSearchPermissionsBody', - 'GitLab Search permission CSVs', - 'The expected revision and complete contents of both UTF-8 CSV files.', + v2ReplaceSearchSourcePermissionsContract.body, + 'ReplaceSearchSourcePermissionsBody', + 'Search source permission grants', + 'The expected revision and complete replacement grants for the provider.', [ { expectedRevision: 2, - userMapping: { - filename: 'users.csv', - content: 'user_id,email\n42,reader@example.com\n', - }, - projectPermissions: { - filename: 'projects.csv', - content: 'project_path,user_id\nteam/project,42\n', + permissions: { + provider: 'gitlab', + userMapping: { + filename: 'users.csv', + content: 'user_id,email\n42,reader@example.com\n', + }, + projectPermissions: { + filename: 'projects.csv', + content: 'project_path,user_id\nteam/project,42\n', + }, }, }, ] ), - response: gitLabSearchPermissionResponse, + response: searchSourcePermissionResponse, } ), ] as const diff --git a/apps/sim/lib/api/contracts/v2/organizations.ts b/apps/sim/lib/api/contracts/v2/organizations.ts index c10244be6bd..07bf61673e8 100644 --- a/apps/sim/lib/api/contracts/v2/organizations.ts +++ b/apps/sim/lib/api/contracts/v2/organizations.ts @@ -1,8 +1,5 @@ import { z } from 'zod' -import { - gitLabPermissionInputSchema, - gitLabPermissionSummarySchema, -} from '@/lib/api/contracts/knowledge/gitlab-permissions' +import { connectorPermissionGrantsSchema } from '@/lib/api/contracts/knowledge/connector-permissions' import { noInputSchema, nonEmptyIdSchema, @@ -323,34 +320,41 @@ export const v2ResendOrganizationInvitationContract = defineRouteContract({ response: { mode: 'json', schema: v2DataResponse(v2OrganizationInvitationSchema) }, }) -const gitLabSearchSourceParamsSchema = v2OrganizationParamsSchema.extend({ - sourceId: nonEmptyIdSchema - .max(255) - .describe('Identifier of the configured GitLab Search source.'), +const searchSourceParamsSchema = v2OrganizationParamsSchema.extend({ + sourceId: nonEmptyIdSchema.max(255).describe('Identifier of the configured Search source.'), }) -const gitLabSearchRevisionResponse = v2DataResponse( - gitLabPermissionSummarySchema.pick({ revision: true }).meta({ - id: 'GitLabSearchPermissionRevision', - title: 'GitLab Search permission revision', - description: 'Revision of the permission CSVs for this source.', - }) +const searchSourceRevisionResponse = v2DataResponse( + z + .object({ revision: z.number().int().nonnegative().describe('Current permission revision.') }) + .meta({ + id: 'SearchSourcePermissionRevision', + title: 'Search source permission revision', + description: 'Revision of this source’s permission grants.', + }) ) -export const v2GetGitLabSearchPermissionsContract = defineRouteContract({ +export const v2GetSearchSourcePermissionsContract = defineRouteContract({ method: 'GET', - path: '/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions', - params: gitLabSearchSourceParamsSchema, + path: '/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions', + params: searchSourceParamsSchema, query: noInputSchema, - response: { mode: 'json', schema: gitLabSearchRevisionResponse }, + response: { mode: 'json', schema: searchSourceRevisionResponse }, }) -export const v2ReplaceGitLabSearchPermissionsContract = defineRouteContract({ +export const v2ReplaceSearchSourcePermissionsContract = defineRouteContract({ method: 'PUT', - path: '/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions', - params: gitLabSearchSourceParamsSchema, + path: '/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions', + params: searchSourceParamsSchema, query: noInputSchema, - body: gitLabPermissionInputSchema - .pick({ expectedRevision: true, userMapping: true, projectPermissions: true }) - .required(), - response: { mode: 'json', schema: gitLabSearchRevisionResponse }, + body: z.strictObject({ + expectedRevision: z + .number() + .int() + .nonnegative() + .describe('Permission revision read before exporting the grants.'), + permissions: connectorPermissionGrantsSchema.describe( + 'Complete permission grants in the source provider’s format.' + ), + }), + response: { mode: 'json', schema: searchSourceRevisionResponse }, }) diff --git a/apps/sim/lib/api/mcp/generated/v2-operations.ts b/apps/sim/lib/api/mcp/generated/v2-operations.ts index 208583dcb47..e21082c9268 100644 --- a/apps/sim/lib/api/mcp/generated/v2-operations.ts +++ b/apps/sim/lib/api/mcp/generated/v2-operations.ts @@ -167,16 +167,16 @@ import { } from '@/lib/api/contracts/v2/organization-usage' import { v2CreateOrganizationInvitationContract, - v2GetGitLabSearchPermissionsContract, v2GetOrganizationContract, v2GetOrganizationInvitationContract, + v2GetSearchSourcePermissionsContract, v2ListOrganizationInvitationsContract, v2ListOrganizationInvitationWorkspacesContract, v2ListOrganizationMembersContract, v2ListOrganizationsContract, v2ListOrganizationWorkspacesContract, v2RemoveOrganizationMemberContract, - v2ReplaceGitLabSearchPermissionsContract, + v2ReplaceSearchSourcePermissionsContract, v2ResendOrganizationInvitationContract, v2RevokeOrganizationInvitationContract, v2UpdateOrganizationMemberContract, @@ -1328,17 +1328,6 @@ export const V2_MCP_OPERATIONS = { handler: () => import('@/app/api/v2/files/[fileId]/versions/[version]/route').then((route) => route.GET), }, - getGitLabSearchPermissions: { - contract: v2GetGitLabSearchPermissionsContract, - summary: 'Get GitLab Search permission revision', - description: - 'Read the current revision of an existing GitLab Search source in CSV permission mode. Requires organization Admin access. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.', - workspaceKeyUnsupported: true, - handler: () => - import( - '@/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions/route' - ).then((route) => route.GET), - }, getKnowledgeBase: { contract: v2GetKnowledgeBaseContract, summary: 'Get knowledge base', @@ -1519,6 +1508,17 @@ export const V2_MCP_OPERATIONS = { 'Get a sandbox’s dependency configuration, current build state, and any build failure. Requires workspace read access.\n\nOAuth scope: `api:read`.', handler: () => import('@/app/api/v2/sandboxes/[sandboxId]/route').then((route) => route.GET), }, + getSearchSourcePermissions: { + contract: v2GetSearchSourcePermissionsContract, + summary: 'Get Search source permission revision', + description: + 'Read the permission revision for a Search source that accepts externally managed grants. Requires organization Admin access. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.', + workspaceKeyUnsupported: true, + handler: () => + import( + '@/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route' + ).then((route) => route.GET), + }, getSelector: { contract: v2GetSelectorContract, summary: 'Get selector option', @@ -2451,15 +2451,15 @@ export const V2_MCP_OPERATIONS = { 'Rename a workspace file without changing its containing folder.\n\nOAuth scope: `api:write`.', handler: () => import('@/app/api/v2/files/[fileId]/route').then((route) => route.PATCH), }, - replaceGitLabSearchPermissions: { - contract: v2ReplaceGitLabSearchPermissionsContract, - summary: 'Replace GitLab Search permission CSVs', + replaceSearchSourcePermissions: { + contract: v2ReplaceSearchSourcePermissionsContract, + summary: 'Replace Search source permissions', description: - 'Replace both permission CSVs for an existing GitLab Search source in CSV mode. Requires organization Admin access. Supply the current revision. A concurrent change returns 409 and leaves both files untouched. Each file accepts up to 4 MiB and 100,000 data rows. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.', + 'Replace the complete permission grants for one Search source. Requires organization Admin access. Supply the current revision and the grants for the configured provider. A stale revision returns 409. GitLab CSV mode accepts both files together, up to 4 MiB and 100,000 data rows each. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.', workspaceKeyUnsupported: true, handler: () => import( - '@/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions/route' + '@/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route' ).then((route) => route.PUT), }, replaceWorkflowChatDeployment: { diff --git a/apps/sim/lib/api/server/routes/copilot-refused-routes.json b/apps/sim/lib/api/server/routes/copilot-refused-routes.json index 4e35fa54e2e..399e897eeb8 100644 --- a/apps/sim/lib/api/server/routes/copilot-refused-routes.json +++ b/apps/sim/lib/api/server/routes/copilot-refused-routes.json @@ -176,13 +176,13 @@ }, { "method": "GET", - "path": "/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions", - "operation": "knowledge.search.gitlab_permissions.read" + "path": "/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions", + "operation": "knowledge.search.source_permissions.read" }, { "method": "PUT", - "path": "/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions", - "operation": "knowledge.search.gitlab_permissions.replace" + "path": "/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions", + "operation": "knowledge.search.source_permissions.replace" }, { "method": "GET", diff --git a/apps/sim/lib/api/server/routes/v2-route-table.generated.ts b/apps/sim/lib/api/server/routes/v2-route-table.generated.ts index ecc078072df..e4072fe5a65 100644 --- a/apps/sim/lib/api/server/routes/v2-route-table.generated.ts +++ b/apps/sim/lib/api/server/routes/v2-route-table.generated.ts @@ -447,10 +447,10 @@ export const V2_ROUTES: readonly V2RouteEntry[] = [ ), }, { - pattern: '/api/v2/organizations/{organizationId}/search/sources/{sourceId}/gitlab-permissions', + pattern: '/api/v2/organizations/{organizationId}/search/sources/{sourceId}/permissions', load: () => import( - '@/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions/route' + '@/app/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions/route' ), }, { diff --git a/apps/sim/lib/knowledge/application/operations.ts b/apps/sim/lib/knowledge/application/operations.ts index 2b81f1f0f47..32d98788516 100644 --- a/apps/sim/lib/knowledge/application/operations.ts +++ b/apps/sim/lib/knowledge/application/operations.ts @@ -1031,19 +1031,19 @@ function defineKnowledgeSessionOperation( return Object.freeze(operation) } -export const gitLabSearchPermissionOperations = { +export const searchSourcePermissionOperations = { read: defineOrganizationOperation({ minimumRole: 'admin', principalKinds: ['personal_api_key', 'oauth_access_token'], capability: 'knowledge.use', - id: 'knowledge.search.gitlab_permissions.read', + id: 'knowledge.search.source_permissions.read', oauthScope: 'api:read', }), replace: defineOrganizationOperation({ minimumRole: 'admin', principalKinds: ['personal_api_key', 'oauth_access_token'], capability: 'knowledge.use', - id: 'knowledge.search.gitlab_permissions.replace', + id: 'knowledge.search.source_permissions.replace', oauthScope: 'api:write', }), } as const diff --git a/apps/sim/lib/knowledge/application/gitlab-search-permissions.ts b/apps/sim/lib/knowledge/application/source-permissions.ts similarity index 57% rename from apps/sim/lib/knowledge/application/gitlab-search-permissions.ts rename to apps/sim/lib/knowledge/application/source-permissions.ts index baaffdcc3c8..4c82b5031e7 100644 --- a/apps/sim/lib/knowledge/application/gitlab-search-permissions.ts +++ b/apps/sim/lib/knowledge/application/source-permissions.ts @@ -6,6 +6,7 @@ import { knowledgeConnectorPermissionSnapshot, } from '@sim/db/schema' import { and, eq, inArray, isNull } from 'drizzle-orm' +import type { ConnectorPermissionGrants } from '@/lib/api/contracts/knowledge/connector-permissions' import { defineAuthorizedOrganizationUseCase, type OrganizationUseCaseContext, @@ -14,18 +15,13 @@ import { authorizeOrganizationOperation } from '@/lib/core/application/organizat import { OrchestrationError } from '@/lib/core/orchestration/types' import { resourceScopeCondition } from '@/lib/core/resource-scope.server' import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability' -import { gitLabSearchPermissionOperations } from '@/lib/knowledge/application/operations' +import { searchSourcePermissionOperations } from '@/lib/knowledge/application/operations' +import { writeConnectorPermissions } from '@/lib/knowledge/connectors/permission-store' import { connectorIsLive } from '@/lib/knowledge/connectors/sync-lock' import { lockOrganizationSearchApproval, searchIntegrationAccessCondition, } from '@/lib/knowledge/search/integration-policy' -import { - prepareGitLabPermissions, - readGitLabPermissionMetadata, - writeGitLabPermissions, -} from '@/connectors/gitlab/permission-config/repository' -import type { GitLabCsvUpload } from '@/connectors/gitlab/permission-config/types' interface SourceInput { organizationId: string @@ -33,12 +29,14 @@ interface SourceInput { } interface ReplacementInput extends SourceInput { expectedRevision: number - userMapping: GitLabCsvUpload - projectPermissions: GitLabCsvUpload + permissions: ConnectorPermissionGrants } function sourceNotFound(): never { - throw new OrchestrationError('not_found', 'GitLab CSV Search source not found') + throw new OrchestrationError( + 'not_found', + 'Search source with externally managed permissions not found' + ) } function searchIndex(organizationId: string) { @@ -49,10 +47,9 @@ function searchIndex(organizationId: string) { ) } -function gitLabSource(sourceId: string) { +function searchSource(sourceId: string) { return and( eq(knowledgeConnector.id, sourceId), - eq(knowledgeConnector.connectorType, 'gitlab'), eq(knowledgeConnector.accessMode, 'admin'), inArray(knowledgeConnector.status, ['active', 'paused', 'pending', 'syncing', 'error']), connectorIsLive(), @@ -60,55 +57,55 @@ function gitLabSource(sourceId: string) { ) } -async function readCsvSource({ organizationId, sourceId }: SourceInput) { +async function readPermissionSource({ organizationId, sourceId }: SourceInput) { if (!(await isKnowledgeMemberAccessAvailable({ organizationId }))) return sourceNotFound() const snapshot = knowledgeConnectorPermissionSnapshot const [source] = await db .select({ knowledgeBaseId: knowledgeBase.id, + connectorType: knowledgeConnector.connectorType, revision: snapshot.revision, metadata: snapshot.metadata, }) .from(knowledgeConnector) .innerJoin(knowledgeBase, eq(knowledgeBase.id, knowledgeConnector.knowledgeBaseId)) .innerJoin(snapshot, eq(snapshot.connectorId, knowledgeConnector.id)) - .where(and(searchIndex(organizationId), gitLabSource(sourceId))) + .where(and(searchIndex(organizationId), searchSource(sourceId))) .limit(1) if (!source) return sourceNotFound() - const metadata = readGitLabPermissionMetadata(source.metadata) - if (metadata.mode !== 'csv') return sourceNotFound() - return { - knowledgeBaseId: source.knowledgeBaseId, - permissions: { ...metadata, revision: source.revision }, - } + const { CONNECTOR_REGISTRY } = await import('@/connectors/registry.server') + const replacement = CONNECTOR_REGISTRY[source.connectorType]?.permissionConfig?.grantReplacement + if (!replacement?.supports(source.metadata)) return sourceNotFound() + return { ...source, replacement } } /** Allows CI to pin the revision without disclosing uploaded identities or project memberships. */ -export const readGitLabSearchPermissions = defineAuthorizedOrganizationUseCase({ - operation: gitLabSearchPermissionOperations.read, +export const readSearchSourcePermissions = defineAuthorizedOrganizationUseCase({ + operation: searchSourcePermissionOperations.read, async execute({ input }: OrganizationUseCaseContext) { - const { permissions } = await readCsvSource(input) - return { revision: permissions.revision } + const { revision } = await readPermissionSource(input) + return { revision } }, }) -/** Replaces both permission files and the grants they produce in one revision. */ -export const replaceGitLabSearchPermissions = defineAuthorizedOrganizationUseCase({ - operation: gitLabSearchPermissionOperations.replace, +/** Replaces the configured provider’s grants in one revision. */ +export const replaceSearchSourcePermissions = defineAuthorizedOrganizationUseCase({ + operation: searchSourcePermissionOperations.replace, async execute({ principal, input }: OrganizationUseCaseContext) { - const { organizationId, sourceId, expectedRevision, userMapping, projectPermissions } = input - const { knowledgeBaseId, permissions } = await readCsvSource(input) - const prepared = prepareGitLabPermissions( - { mode: 'csv', expectedRevision, userMapping, projectPermissions }, - permissions, - permissions, - true - ) - await db.transaction(async (tx) => { + const { organizationId, sourceId, expectedRevision, permissions } = input + const source = await readPermissionSource(input) + if (permissions.provider !== source.connectorType) { + throw new OrchestrationError( + 'validation', + 'The permissions must match the configured source provider.' + ) + } + const prepared = source.replacement.prepare(source, expectedRevision, permissions) + const revision = await db.transaction(async (tx) => { await lockOrganizationSearchApproval(tx, organizationId) await authorizeOrganizationOperation( principal, - gitLabSearchPermissionOperations.replace, + searchSourcePermissionOperations.replace, input, { executor: tx, @@ -119,25 +116,31 @@ export const replaceGitLabSearchPermissions = defineAuthorizedOrganizationUseCas const [index] = await tx .select({ id: knowledgeBase.id }) .from(knowledgeBase) - .where(and(eq(knowledgeBase.id, knowledgeBaseId), searchIndex(organizationId))) + .where(and(eq(knowledgeBase.id, source.knowledgeBaseId), searchIndex(organizationId))) .for('share') if (!index) return sourceNotFound() - const [source] = await tx + const [locked] = await tx .select({ id: knowledgeConnector.id }) .from(knowledgeConnector) - .where(and(eq(knowledgeConnector.knowledgeBaseId, index.id), gitLabSource(sourceId))) + .where( + and( + eq(knowledgeConnector.knowledgeBaseId, index.id), + eq(knowledgeConnector.connectorType, source.connectorType), + searchSource(sourceId) + ) + ) .for('update') - if (!source) return sourceNotFound() - await writeGitLabPermissions(tx, source.id, prepared) + if (!locked) return sourceNotFound() + return writeConnectorPermissions(tx, locked.id, { ...prepared, expectedRevision }) }) - return { revision: prepared.snapshot.revision } + return { revision } }, projectAudit: ({ input, result }) => ({ action: AuditAction.CONNECTOR_UPDATED, resourceType: AuditResourceType.CONNECTOR, resourceId: input.sourceId, - resourceName: 'gitlab', - description: 'Replaced GitLab Search permission CSVs', + resourceName: input.permissions.provider, + description: 'Replaced Search source permissions', metadata: { revision: result.revision }, }), }) diff --git a/apps/sim/lib/knowledge/connectors/permission-config.ts b/apps/sim/lib/knowledge/connectors/permission-config.ts index 828fd7ef0c4..d38cd24b318 100644 --- a/apps/sim/lib/knowledge/connectors/permission-config.ts +++ b/apps/sim/lib/knowledge/connectors/permission-config.ts @@ -1,4 +1,9 @@ +import type { ConnectorPermissionGrants } from '@/lib/api/contracts/knowledge/connector-permissions' import type { DbTransaction } from '@/lib/db/types' +import type { + ConnectorPermissionData, + ConnectorPermissionSnapshot, +} from '@/lib/knowledge/connectors/permission-store' import type { GitLabPermissionInput, GitLabPermissionSummary, @@ -31,6 +36,14 @@ export interface PreparedConnectorPermissions { /** Providers own private configuration and grants; orchestration owns the surrounding transaction. */ export interface ConnectorPermissionConfigCapability { + grantReplacement?: { + supports(metadata: unknown): boolean + prepare( + snapshot: Pick, + expectedRevision: number, + permissions: ConnectorPermissionGrants + ): ConnectorPermissionData + } prepare( input: PrepareConnectorPermissionsInput ): Promise diff --git a/apps/sim/lib/knowledge/connectors/permission-store.ts b/apps/sim/lib/knowledge/connectors/permission-store.ts index 83f1b4b9da9..ed3154e95f6 100644 --- a/apps/sim/lib/knowledge/connectors/permission-store.ts +++ b/apps/sim/lib/knowledge/connectors/permission-store.ts @@ -9,6 +9,12 @@ import type { DbTransaction } from '@/lib/db/types' export type ConnectorPermissionSnapshot = typeof knowledgeConnectorPermissionSnapshot.$inferSelect +export interface ConnectorPermissionData { + metadata: Record + payload: Record + groups: readonly { groupKey: string; subjects: readonly string[] }[] +} + /** The application has authorized this canonical source and verified the reader's subject. */ export async function hasConnectorPermissionGrant( connectorId: string, @@ -64,12 +70,7 @@ export async function readConnectorPermissionMetadata(connectorIds: readonly str export async function writeConnectorPermissions( tx: DbTransaction, connectorId: string, - input: { - expectedRevision: number - metadata: Record - payload: Record - groups: readonly { groupKey: string; subjects: readonly string[] }[] - } + input: ConnectorPermissionData & { expectedRevision: number } ) { const table = knowledgeConnectorPermissionSnapshot const [current] = await tx @@ -114,4 +115,5 @@ export async function writeConnectorPermissions( 'Could not save permissions. The previous configuration was preserved.' ) } + return values.revision } diff --git a/apps/sim/scripts/test-gitlab-csv-permissions-e2e.ts b/apps/sim/scripts/test-gitlab-csv-permissions-e2e.ts index ae639a9595a..9829326af7a 100644 --- a/apps/sim/scripts/test-gitlab-csv-permissions-e2e.ts +++ b/apps/sim/scripts/test-gitlab-csv-permissions-e2e.ts @@ -35,17 +35,20 @@ const file = (filename: string, content: string) => ({ filename, content }) const labels = { filename: 'initial.csv', rowCount: 1, uploadedAt: new Date().toISOString() } const csv = (expectedRevision: number, id = 2) => ({ expectedRevision, - userMapping: file( - 'users.csv', - `user_id,email\n1,${initialEmail}\n2,${nextEmail.toUpperCase()}\n` - ), - projectPermissions: file( - 'projects.csv', - `project_path,user_id\n${project},${id}\nelsewhere/team,1\n` - ), + permissions: { + provider: 'gitlab', + userMapping: file( + 'users.csv', + `user_id,email\n1,${initialEmail}\n2,${nextEmail.toUpperCase()}\n` + ), + projectPermissions: file( + 'projects.csv', + `project_path,user_id\n${project},${id}\nelsewhere/team,1\n` + ), + }, }) const path = (organizationId = org) => - `/api/v2/organizations/${organizationId}/search/sources/${source}/gitlab-permissions` + `/api/v2/organizations/${organizationId}/search/sources/${source}/permissions` async function request(token: string, body?: unknown, organizationId = org) { return fetch(new URL(path(organizationId), base), { @@ -122,7 +125,12 @@ try { } assert.equal((await request(otherAdminKey, csv(1), otherOrg)).status, 404) assert.equal( - (await request(adminKey, { expectedRevision: 1, userMapping: csv(1).userMapping })).status, + ( + await request(adminKey, { + expectedRevision: 1, + permissions: { provider: 'gitlab', userMapping: csv(1).permissions.userMapping }, + }) + ).status, 400 ) assert.equal((await request(adminKey, { ...csv(1), apiKey: 'unexpected' })).status, 400) @@ -130,13 +138,30 @@ try { ( await request(adminKey, { ...csv(1), - userMapping: file('users.csv', 'x'.repeat(10 * 1024 * 1024)), + permissions: { + ...csv(1).permissions, + userMapping: file('users.csv', 'x'.repeat(10 * 1024 * 1024)), + }, }) ).status, 413 ) assert.equal( - (await request(adminKey, { ...csv(1), userMapping: file('users.csv', '1,invalid') })).status, + ( + await request(adminKey, { + ...csv(1), + permissions: { ...csv(1).permissions, userMapping: file('users.csv', '1,invalid') }, + }) + ).status, + 400 + ) + assert.equal( + ( + await request(adminKey, { + ...csv(1), + permissions: { ...csv(1).permissions, provider: 'unknown' }, + }) + ).status, 400 ) assert.deepEqual(await grants(), [`u:${initialEmail}`]) @@ -146,6 +171,7 @@ try { ) await sql`update knowledge_connector set connector_type = 'github' where id = ${source}` + assert.equal((await request(adminKey)).status, 404) assert.equal((await request(adminKey, csv(1))).status, 404) await sql`update knowledge_connector set connector_type = 'gitlab' where id = ${source}` const [original] = @@ -154,6 +180,7 @@ try { await sql`update knowledge_connector_permission_snapshot set metadata = metadata || '{"mode":"administrator","userMapping":null,"projectPermissions":null}'::jsonb where connector_id = ${source}` + assert.equal((await request(adminKey)).status, 404) assert.equal((await request(adminKey, csv(1))).status, 404) await sql`update knowledge_connector_permission_snapshot set metadata = ${sql.json(original.metadata)} where connector_id = ${source}` assert.deepEqual(await grants(), [`u:${initialEmail}`]) diff --git a/packages/sim-cli/src/contract/commands.ts b/packages/sim-cli/src/contract/commands.ts index 78baeda7163..8118dd99ca6 100644 --- a/packages/sim-cli/src/contract/commands.ts +++ b/packages/sim-cli/src/contract/commands.ts @@ -1238,15 +1238,15 @@ export const CLI_CONTRACT: CliContract = { columns: [{ header: 'id' }, { header: 'name' }, { header: 'role' }], }, getOrganization: { command: 'organizations get' }, - getGitLabSearchPermissions: { - command: 'organizations gitlab-permissions get', + getSearchSourcePermissions: { + command: 'organizations search sources permissions get', pathFlags: ORGANIZATION_FLAG, }, - replaceGitLabSearchPermissions: { - command: 'organizations gitlab-permissions replace', + replaceSearchSourcePermissions: { + command: 'organizations search sources permissions replace', pathFlags: ORGANIZATION_FLAG, confirm: - 'This replaces both GitLab permission files. Readers omitted from the new grants lose access.', + 'This replaces the source’s permission grants. Readers omitted from the new grants lose access.', }, listSsoProviders: { command: 'organizations sso providers list', diff --git a/packages/sim-cli/src/generated/v2-api.ts b/packages/sim-cli/src/generated/v2-api.ts index 12f01c3613a..992fa1a7246 100644 --- a/packages/sim-cli/src/generated/v2-api.ts +++ b/packages/sim-cli/src/generated/v2-api.ts @@ -6221,22 +6221,6 @@ export type GetFileVersionResponse = { data: GetFileVersionResponseRef0 } -/** `GET /api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions` */ -export type GetGitLabSearchPermissionsParams = { - organizationId: string - sourceId: string -} - -export type GetGitLabSearchPermissionsQuery = Record - -type GetGitLabSearchPermissionsResponseRef0 = { - revision: number -} - -export type GetGitLabSearchPermissionsResponse = { - data: GetGitLabSearchPermissionsResponseRef0 -} - /** `GET /api/v2/knowledge/[knowledgeBaseId]` */ export type GetKnowledgeBaseParams = { knowledgeBaseId: string @@ -7176,6 +7160,22 @@ export type GetSandboxResponse = { data: GetSandboxResponseRef0 } +/** `GET /api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions` */ +export type GetSearchSourcePermissionsParams = { + organizationId: string + sourceId: string +} + +export type GetSearchSourcePermissionsQuery = Record + +type GetSearchSourcePermissionsResponseRef0 = { + revision: number +} + +export type GetSearchSourcePermissionsResponse = { + data: GetSearchSourcePermissionsResponseRef0 +} + /** `POST /api/v2/selectors/get` */ export type GetSelectorQuery = Record @@ -12741,32 +12741,35 @@ export type RenameProjectFileResponse = { data: RenameProjectFileResponseRef0 } -/** `PUT /api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions` */ -export type ReplaceGitLabSearchPermissionsParams = { +/** `PUT /api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions` */ +export type ReplaceSearchSourcePermissionsParams = { organizationId: string sourceId: string } -export type ReplaceGitLabSearchPermissionsQuery = Record +export type ReplaceSearchSourcePermissionsQuery = Record -export type ReplaceGitLabSearchPermissionsBody = { +export type ReplaceSearchSourcePermissionsBody = { expectedRevision: number - userMapping: { - filename: string - content: string - } - projectPermissions: { - filename: string - content: string + permissions: { + provider: 'gitlab' + userMapping: { + filename: string + content: string + } + projectPermissions: { + filename: string + content: string + } } } -type ReplaceGitLabSearchPermissionsResponseRef0 = { +type ReplaceSearchSourcePermissionsResponseRef0 = { revision: number } -export type ReplaceGitLabSearchPermissionsResponse = { - data: ReplaceGitLabSearchPermissionsResponseRef0 +export type ReplaceSearchSourcePermissionsResponse = { + data: ReplaceSearchSourcePermissionsResponseRef0 } /** `PUT /api/v2/workflows/[workflowId]/deployments/chat` */ @@ -18837,18 +18840,6 @@ export const V2_OPERATIONS = { workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the file.' }, }, }, - getGitLabSearchPermissions: { - method: 'GET', - path: '/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions', - pathParams: ['organizationId', 'sourceId'] as const, - pathParamDocs: { - organizationId: 'Organization identifier.', - sourceId: 'Identifier of the configured GitLab Search source.', - }, - responseMode: 'json', - summary: 'Get GitLab Search permission revision', - workspaceKeyUnsupported: true, - }, getKnowledgeBase: { method: 'GET', path: '/api/v2/knowledge/[knowledgeBaseId]', @@ -19319,6 +19310,18 @@ export const V2_OPERATIONS = { workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the sandbox.' }, }, }, + getSearchSourcePermissions: { + method: 'GET', + path: '/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions', + pathParams: ['organizationId', 'sourceId'] as const, + pathParamDocs: { + organizationId: 'Organization identifier.', + sourceId: 'Identifier of the configured Search source.', + }, + responseMode: 'json', + summary: 'Get Search source permission revision', + workspaceKeyUnsupported: true, + }, getSelector: { method: 'POST', path: '/api/v2/selectors/get', @@ -23129,32 +23132,27 @@ export const V2_OPERATIONS = { name: { kind: 'string', required: true, describe: 'New file name, including its extension.' }, }, }, - replaceGitLabSearchPermissions: { + replaceSearchSourcePermissions: { method: 'PUT', - path: '/api/v2/organizations/[organizationId]/search/sources/[sourceId]/gitlab-permissions', + path: '/api/v2/organizations/[organizationId]/search/sources/[sourceId]/permissions', pathParams: ['organizationId', 'sourceId'] as const, pathParamDocs: { organizationId: 'Organization identifier.', - sourceId: 'Identifier of the configured GitLab Search source.', + sourceId: 'Identifier of the configured Search source.', }, responseMode: 'json', - summary: 'Replace GitLab Search permission CSVs', + summary: 'Replace Search source permissions', workspaceKeyUnsupported: true, body: { expectedRevision: { kind: 'integer', required: true, - describe: 'Current permission revision obtained before generating this snapshot.', - }, - userMapping: { - kind: 'object', - required: true, - describe: 'GitLab user IDs and their verified Sim email addresses.', + describe: 'Permission revision read before exporting the grants.', }, - projectPermissions: { + permissions: { kind: 'object', required: true, - describe: 'GitLab project paths and the IDs of users granted access.', + describe: 'Complete permission grants in the source provider’s format.', }, }, }, diff --git a/packages/sim-cli/src/http/client.test.ts b/packages/sim-cli/src/http/client.test.ts index 91c9f17ba31..97fafa33d86 100644 --- a/packages/sim-cli/src/http/client.test.ts +++ b/packages/sim-cli/src/http/client.test.ts @@ -407,7 +407,7 @@ describe('destructive operations are gated', () => { 'bulkDeleteTables', 'bulkUpdateKnowledgeChunks', 'replaceWorkflowChatDeployment', - 'replaceGitLabSearchPermissions', + 'replaceSearchSourcePermissions', 'replaceWorkflowState', 'revertWorkflowVersion', 'updateRowsByFilter', diff --git a/scripts/check-unused-exports.baseline.json b/scripts/check-unused-exports.baseline.json index bc1a0fedf63..c88c99af1ff 100644 --- a/scripts/check-unused-exports.baseline.json +++ b/scripts/check-unused-exports.baseline.json @@ -4519,7 +4519,6 @@ "apps/sim/lib/knowledge/connectors/external-group-sync.ts#DIRECTORY_ERROR_PREFIX", "apps/sim/lib/knowledge/connectors/external-group-sync.ts#DIRECTORY_WARNING_PREFIX", "apps/sim/lib/knowledge/connectors/external-group-sync.ts#replaceDirectorySyncNotice", - "apps/sim/lib/knowledge/connectors/permission-store.ts#ConnectorPermissionSnapshot", "apps/sim/lib/knowledge/connectors/sync-database-retry.ts#RUN_HISTORY_LOCK_TIMEOUT_MS", "apps/sim/lib/knowledge/connectors/sync-database-retry.ts#RUN_HISTORY_STATEMENT_TIMEOUT_MS", "apps/sim/lib/knowledge/connectors/sync-engine.ts#ConnectorCredentialRevokedError", diff --git a/scripts/generate-cli-docs.ts b/scripts/generate-cli-docs.ts index 762d961e453..762bf4ee069 100644 --- a/scripts/generate-cli-docs.ts +++ b/scripts/generate-cli-docs.ts @@ -308,7 +308,7 @@ const GROUP_SECTIONS: Record = { organizations: [ { title: 'Organizations and workspaces', prefixes: ['list', 'get', 'workspaces'] }, { title: 'Members and credit limits', prefixes: ['members'] }, - { title: 'GitLab Search permissions', prefixes: ['gitlab-permissions'] }, + { title: 'Search source permissions', prefixes: ['search sources permissions'] }, { title: 'Invitations', prefixes: ['invitations'] }, { title: 'Usage', prefixes: ['usage summary', 'usage breakdown', 'usage events'] }, { From ef17ff30f468c03cd4f61218ff18ab32b1ed4dd5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 10 Nov 2026 11:11:52 -0800 Subject: [PATCH 2/2] chore(search): document the shared permission grant format --- apps/sim/connectors/gitlab/permission-config/repository.ts | 1 + apps/sim/lib/api/contracts/knowledge/connector-permissions.ts | 1 + apps/sim/lib/knowledge/connectors/permission-store.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/apps/sim/connectors/gitlab/permission-config/repository.ts b/apps/sim/connectors/gitlab/permission-config/repository.ts index 65c3ffbdd9e..4cdab7b03ba 100644 --- a/apps/sim/connectors/gitlab/permission-config/repository.ts +++ b/apps/sim/connectors/gitlab/permission-config/repository.ts @@ -172,6 +172,7 @@ export function prepareGitLabPermissions( } } +/** Keeps CSV rows in the private payload and stores only the configured project’s reader grants. */ export function gitLabPermissionData(prepared: PreparedGitLabPermissions): ConnectorPermissionData { const { revision: _revision, diff --git a/apps/sim/lib/api/contracts/knowledge/connector-permissions.ts b/apps/sim/lib/api/contracts/knowledge/connector-permissions.ts index b81df83923a..b4114e503bb 100644 --- a/apps/sim/lib/api/contracts/knowledge/connector-permissions.ts +++ b/apps/sim/lib/api/contracts/knowledge/connector-permissions.ts @@ -12,6 +12,7 @@ export const connectorPermissionGrantsSchema = z.discriminatedUnion('provider', .pick({ provider: true, userMapping: true, projectPermissions: true }) .required(), ]) +/** Provider-specific grants that an organization admin can replace without changing source settings. */ export type ConnectorPermissionGrants = z.output export const connectorPermissionSummarySchema = z.discriminatedUnion('provider', [ diff --git a/apps/sim/lib/knowledge/connectors/permission-store.ts b/apps/sim/lib/knowledge/connectors/permission-store.ts index ed3154e95f6..3979a617dc3 100644 --- a/apps/sim/lib/knowledge/connectors/permission-store.ts +++ b/apps/sim/lib/knowledge/connectors/permission-store.ts @@ -9,6 +9,7 @@ import type { DbTransaction } from '@/lib/db/types' export type ConnectorPermissionSnapshot = typeof knowledgeConnectorPermissionSnapshot.$inferSelect +/** A complete replacement. Metadata can be read without loading the private payload or subject grants. */ export interface ConnectorPermissionData { metadata: Record payload: Record