diff --git a/.env.example b/.env.example index 23f845b71..53e36449a 100644 --- a/.env.example +++ b/.env.example @@ -107,6 +107,10 @@ INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY= INF_APP_CONNECTION_GITHUB_APP_SLUG= INF_APP_CONNECTION_GITHUB_APP_ID= +#gitlab app connection +INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID= +INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET= + #github radar app connection INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID= INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET= diff --git a/backend/package-lock.json b/backend/package-lock.json index 49df5a596..b90e448cb 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -30,6 +30,7 @@ "@fastify/static": "^7.0.4", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", + "@gitbeaker/rest": "^42.5.0", "@google-cloud/kms": "^4.5.0", "@infisical/quic": "^1.0.8", "@node-saml/passport-saml": "^5.0.1", @@ -7807,6 +7808,48 @@ "p-limit": "^3.1.0" } }, + "node_modules/@gitbeaker/core": { + "version": "42.5.0", + "resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-42.5.0.tgz", + "integrity": "sha512-rMWpOPaZi1iLiifnOIoVO57p2EmQQdfIwP4txqNyMvG4WjYP5Ez0U7jRD9Nra41x6K5kTPBZkuQcAdxVWRJcEQ==", + "license": "MIT", + "dependencies": { + "@gitbeaker/requester-utils": "^42.5.0", + "qs": "^6.12.2", + "xcase": "^2.0.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@gitbeaker/requester-utils": { + "version": "42.5.0", + "resolved": "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-42.5.0.tgz", + "integrity": "sha512-HLdLS9LPBMVQumvroQg/4qkphLDtwDB+ygEsrD2u4oYCMUtXV4V1xaVqU4yTXjbTJ5sItOtdB43vYRkBcgueBw==", + "license": "MIT", + "dependencies": { + "picomatch-browser": "^2.2.6", + "qs": "^6.12.2", + "rate-limiter-flexible": "^4.0.1", + "xcase": "^2.0.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@gitbeaker/rest": { + "version": "42.5.0", + "resolved": "https://registry.npmjs.org/@gitbeaker/rest/-/rest-42.5.0.tgz", + "integrity": "sha512-oC5cM6jS7aFOp0luTw5mWSRuMgdxwHRLZQ/aWkI+ETMfsprR/HyxsXfljlMY/XJ/fRxTbRJiodR5Axf66WjO3w==", + "license": "MIT", + "dependencies": { + "@gitbeaker/core": "^42.5.0", + "@gitbeaker/requester-utils": "^42.5.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, "node_modules/@google-cloud/kms": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz", @@ -24628,6 +24671,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/picomatch-browser": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/picomatch-browser/-/picomatch-browser-2.2.6.tgz", + "integrity": "sha512-0ypsOQt9D4e3hziV8O4elD9uN0z/jtUEfxVRtNaAAtXIyUx9m/SzlO020i8YNL2aL/E6blOvvHQcin6HZlFy/w==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -25562,6 +25617,12 @@ "node": ">= 0.6" } }, + "node_modules/rate-limiter-flexible": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-4.0.1.tgz", + "integrity": "sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==", + "license": "ISC" + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -31039,6 +31100,12 @@ } } }, + "node_modules/xcase": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xcase/-/xcase-2.0.1.tgz", + "integrity": "sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==", + "license": "MIT" + }, "node_modules/xml-crypto": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index c2bfc29d9..128de7bd6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -149,6 +149,7 @@ "@fastify/static": "^7.0.4", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", + "@gitbeaker/rest": "^42.5.0", "@google-cloud/kms": "^4.5.0", "@infisical/quic": "^1.0.8", "@node-saml/passport-saml": "^5.0.1", diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b4892a209..be460b4b4 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2228,6 +2228,12 @@ export const AppConnections = { }, FLYIO: { accessToken: "The Access Token used to access fly.io." + }, + GITLAB: { + instanceUrl: "The GitLab instance URL to connect with.", + accessToken: "The Access Token used to access GitLab.", + code: "The OAuth code to use to connect with GitLab.", + accessTokenType: "The type of token used to connect with GitLab." } } }; @@ -2402,6 +2408,17 @@ export const SecretSyncs = { FLYIO: { appId: "The ID of the Fly.io app to sync secrets to." }, + GITLAB: { + projectId: "The GitLab Project ID to sync secrets to.", + projectName: "The GitLab Project Name to sync secrets to.", + groupId: "The GitLab Group ID to sync secrets to.", + groupName: "The GitLab Group Name to sync secrets to.", + scope: "The GitLab scope that secrets should be synced to. (default: project)", + targetEnvironment: "The GitLab environment scope that secrets should be synced to. (default: *)", + shouldProtectSecrets: "Whether variables should be protected", + shouldMaskSecrets: "Whether variables should be masked in logs", + shouldHideSecrets: "Whether variables should be hidden" + }, CLOUDFLARE_PAGES: { projectName: "The name of the Cloudflare Pages project to sync secrets to.", environment: "The environment of the Cloudflare Pages project to sync secrets to." diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index b0b35cc2f..4fb19e7bb 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -247,6 +247,10 @@ const envSchema = z INF_APP_CONNECTION_GITHUB_RADAR_APP_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET: zpStr(z.string().optional()), + // gitlab oauth + INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()), + // gcp app INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()), diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index dcf8bf2ce..6160828f4 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -35,6 +35,10 @@ import { CamundaConnectionListItemSchema, SanitizedCamundaConnectionSchema } from "@app/services/app-connection/camunda"; +import { + CloudflareConnectionListItemSchema, + SanitizedCloudflareConnectionSchema +} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema"; import { DatabricksConnectionListItemSchema, SanitizedDatabricksConnectionSchema @@ -46,6 +50,7 @@ import { GitHubRadarConnectionListItemSchema, SanitizedGitHubRadarConnectionSchema } from "@app/services/app-connection/github-radar"; +import { GitLabConnectionListItemSchema, SanitizedGitLabConnectionSchema } from "@app/services/app-connection/gitlab"; import { HCVaultConnectionListItemSchema, SanitizedHCVaultConnectionSchema @@ -80,10 +85,6 @@ import { WindmillConnectionListItemSchema } from "@app/services/app-connection/windmill"; import { AuthMode } from "@app/services/auth/auth-type"; -import { - CloudflareConnectionListItemSchema, - SanitizedCloudflareConnectionSchema -} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema"; // can't use discriminated due to multiple schemas for certain apps const SanitizedAppConnectionSchema = z.union([ @@ -114,6 +115,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedHerokuConnectionSchema.options, ...SanitizedRenderConnectionSchema.options, ...SanitizedFlyioConnectionSchema.options, + ...SanitizedGitLabConnectionSchema.options, ...SanitizedCloudflareConnectionSchema.options ]); @@ -145,6 +147,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ HerokuConnectionListItemSchema, RenderConnectionListItemSchema, FlyioConnectionListItemSchema, + GitLabConnectionListItemSchema, CloudflareConnectionListItemSchema ]); diff --git a/backend/src/server/routes/v1/app-connection-routers/gitlab-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/gitlab-connection-router.ts new file mode 100644 index 000000000..b168cc97f --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/gitlab-connection-router.ts @@ -0,0 +1,90 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateGitLabConnectionSchema, + SanitizedGitLabConnectionSchema, + TGitLabGroup, + TGitLabProject, + UpdateGitLabConnectionSchema +} from "@app/services/app-connection/gitlab"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerGitLabConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.GitLab, + server, + sanitizedResponseSchema: SanitizedGitLabConnectionSchema, + createSchema: CreateGitLabConnectionSchema, + updateSchema: UpdateGitLabConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/projects`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const projects: TGitLabProject[] = await server.services.appConnection.gitlab.listProjects( + connectionId, + req.permission + ); + + return projects; + } + }); + + server.route({ + method: "GET", + url: `/:connectionId/groups`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const groups: TGitLabGroup[] = await server.services.appConnection.gitlab.listGroups( + connectionId, + req.permission + ); + + return groups; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index d8f2b05a2..cd4ccd728 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -10,11 +10,13 @@ import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secre import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; +import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router"; +import { registerGitLabConnectionRouter } from "./gitlab-connection-router"; import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHerokuConnectionRouter } from "./heroku-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; @@ -27,7 +29,6 @@ import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerWindmillConnectionRouter } from "./windmill-connection-router"; -import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; export * from "./app-connection-router"; @@ -60,5 +61,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.GitLab, + server, + responseSchema: GitLabSyncSchema, + createSchema: CreateGitLabSyncSchema, + updateSchema: UpdateGitLabSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index b55c63a92..4675a1a40 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -13,6 +13,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; +import { registerGitLabSyncRouter } from "./gitlab-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHerokuSyncRouter } from "./heroku-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; @@ -45,5 +46,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { return [ @@ -134,6 +135,7 @@ export const listAppConnectionOptions = () => { getHerokuConnectionListItem(), getRenderConnectionListItem(), getFlyioConnectionListItem(), + getGitLabConnectionListItem(), getCloudflareConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -213,6 +215,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -230,6 +233,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case GitHubConnectionMethod.OAuth: case AzureDevOpsConnectionMethod.OAuth: case HerokuConnectionMethod.OAuth: + case GitLabConnectionMethod.OAuth: return "OAuth"; case HerokuConnectionMethod.AuthToken: return "Auth Token"; @@ -327,6 +331,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Heroku]: platformManagedCredentialsNotSupported, [AppConnection.Render]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported, + [AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 6e735af9f..9c0a3b5b8 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -28,6 +28,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Heroku]: "Heroku", [AppConnection.Render]: "Render", [AppConnection.Flyio]: "Fly.io", + [AppConnection.GitLab]: "GitLab", [AppConnection.Cloudflare]: "Cloudflare" }; @@ -59,5 +60,6 @@ export const APP_CONNECTION_PLAN_MAP: Record { + const { INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID } = getConfig(); + + return { + name: "GitLab" as const, + app: AppConnection.GitLab as const, + methods: Object.values(GitLabConnectionMethod) as [ + GitLabConnectionMethod.AccessToken, + GitLabConnectionMethod.OAuth + ], + oauthClientId: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID + }; +}; + +export const getGitLabInstanceUrl = async (instanceUrl?: string) => { + const gitLabInstanceUrl = instanceUrl ? removeTrailingSlash(instanceUrl) : IntegrationUrls.GITLAB_URL; + + await blockLocalAndPrivateIpAddresses(gitLabInstanceUrl); + + return gitLabInstanceUrl; +}; + +export const getGitLabClient = async (accessToken: string, instanceUrl?: string, isOAuth = false) => { + const host = await getGitLabInstanceUrl(instanceUrl); + + const client = new Gitlab({ + host, + ...(isOAuth ? { oauthToken: accessToken } : { token: accessToken }), + camelize: true + }); + + return client; +}; + +export const refreshGitLabToken = async ( + refreshToken: string, + appId: string, + orgId: string, + appConnectionDAL: Pick, + kmsService: Pick, + instanceUrl?: string +): Promise => { + const { INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET, SITE_URL } = + getConfig(); + if (!INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET || !INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID || !SITE_URL) { + throw new InternalServerError({ + message: `GitLab environment variables have not been configured` + }); + } + + const payload = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID, + client_secret: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET, + redirect_uri: `${SITE_URL}/organization/app-connections/gitlab/oauth/callback` + }); + + try { + const url = await getGitLabInstanceUrl(instanceUrl); + const { data } = await request.post(`${url}/oauth/token`, payload.toString(), { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json" + } + }); + + const expiresAt = new Date(Date.now() + data.expires_in * 1000 - 600000); + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: { + instanceUrl, + tokenType: data.token_type, + createdAt: new Date(data.created_at * 1000).toISOString(), + refreshToken: data.refresh_token, + accessToken: data.access_token, + expiresAt + }, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(appId, { encryptedCredentials }); + return data.access_token; + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to refresh GitLab token: ${error.message}` + }); + } + throw new BadRequestError({ + message: "Unable to refresh GitLab token" + }); + } +}; + +export const exchangeGitLabOAuthCode = async ( + code: string, + instanceUrl?: string +): Promise => { + const { INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET, SITE_URL } = + getConfig(); + if (!INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET || !INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID || !SITE_URL) { + throw new InternalServerError({ + message: `GitLab environment variables have not been configured` + }); + } + + try { + const payload = new URLSearchParams({ + grant_type: "authorization_code", + code, + client_id: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID, + client_secret: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET, + redirect_uri: `${SITE_URL}/organization/app-connections/gitlab/oauth/callback` + }); + const url = await getGitLabInstanceUrl(instanceUrl); + + const response = await request.post(`${url}/oauth/token`, payload.toString(), { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json" + } + }); + + if (!response.data) { + throw new InternalServerError({ + message: "Failed to exchange OAuth code: Empty response" + }); + } + + return response.data; + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to exchange OAuth code: ${error.message}` + }); + } + throw new BadRequestError({ + message: "Unable to exchange OAuth code" + }); + } +}; + +export const validateGitLabConnectionCredentials = async (config: TGitLabConnectionConfig) => { + const { credentials: inputCredentials, method } = config; + + let accessToken: string; + let oauthData: GitLabOAuthTokenResponse | null = null; + + if (method === GitLabConnectionMethod.OAuth && "code" in inputCredentials) { + oauthData = await exchangeGitLabOAuthCode(inputCredentials.code, inputCredentials.instanceUrl); + accessToken = oauthData.access_token; + } else if (method === GitLabConnectionMethod.AccessToken && "accessToken" in inputCredentials) { + accessToken = inputCredentials.accessToken; + } else { + throw new BadRequestError({ + message: "Invalid credentials for the selected connection method" + }); + } + + try { + const client = await getGitLabClient( + accessToken, + inputCredentials.instanceUrl, + method === GitLabConnectionMethod.OAuth + ); + await client.Users.showCurrentUser(); + } catch (error: unknown) { + logger.error(error, "Error validating GitLab connection credentials"); + + if (error instanceof GitbeakerRequestError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}` + }); + } + + throw new BadRequestError({ + message: `Failed to validate credentials: ${(error as Error)?.message || "verify credentials"}` + }); + } + + if (method === GitLabConnectionMethod.OAuth && oauthData) { + return { + accessToken, + instanceUrl: inputCredentials.instanceUrl, + refreshToken: oauthData.refresh_token, + expiresAt: new Date(Date.now() + oauthData.expires_in * 1000 - 60000), + tokenType: oauthData.token_type, + createdAt: new Date(oauthData.created_at * 1000) + }; + } + + return inputCredentials; +}; + +export const listGitLabProjects = async ({ + appConnection, + appConnectionDAL, + kmsService +}: { + appConnection: TGitLabConnection; + appConnectionDAL: Pick; + kmsService: Pick; +}): Promise => { + let { accessToken } = appConnection.credentials; + + if ( + appConnection.method === GitLabConnectionMethod.OAuth && + appConnection.credentials.refreshToken && + new Date(appConnection.credentials.expiresAt) < new Date() + ) { + accessToken = await refreshGitLabToken( + appConnection.credentials.refreshToken, + appConnection.id, + appConnection.orgId, + appConnectionDAL, + kmsService, + appConnection.credentials.instanceUrl + ); + } + + try { + const client = await getGitLabClient( + accessToken, + appConnection.credentials.instanceUrl, + appConnection.method === GitLabConnectionMethod.OAuth + ); + const projects = await client.Projects.all({ + archived: false, + includePendingDelete: false, + membership: true, + includeHidden: false, + imported: false + }); + + return projects.map((project) => ({ + name: project.pathWithNamespace, + id: project.id.toString() + })); + } catch (error: unknown) { + if (error instanceof GitbeakerRequestError) { + throw new BadRequestError({ + message: `Failed to fetch GitLab projects: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}` + }); + } + + if (error instanceof InternalServerError) { + throw error; + } + + throw new InternalServerError({ + message: "Unable to fetch GitLab projects" + }); + } +}; + +export const listGitLabGroups = async ({ + appConnection, + appConnectionDAL, + kmsService +}: { + appConnection: TGitLabConnection; + appConnectionDAL: Pick; + kmsService: Pick; +}): Promise => { + let { accessToken } = appConnection.credentials; + + if ( + appConnection.method === GitLabConnectionMethod.AccessToken && + appConnection.credentials.accessTokenType === GitLabAccessTokenType.Project + ) { + return []; + } + + if ( + appConnection.method === GitLabConnectionMethod.OAuth && + appConnection.credentials.refreshToken && + new Date(appConnection.credentials.expiresAt) < new Date() + ) { + accessToken = await refreshGitLabToken( + appConnection.credentials.refreshToken, + appConnection.id, + appConnection.orgId, + appConnectionDAL, + kmsService, + appConnection.credentials.instanceUrl + ); + } + + try { + const client = await getGitLabClient( + accessToken, + appConnection.credentials.instanceUrl, + appConnection.method === GitLabConnectionMethod.OAuth + ); + + const groups = await client.Groups.all({ + orderBy: "name", + sort: "asc", + minAccessLevel: 50 + }); + + return groups.map((group) => ({ + id: group.id.toString(), + name: group.name + })); + } catch (error: unknown) { + if (error instanceof GitbeakerRequestError) { + throw new BadRequestError({ + message: `Failed to fetch GitLab groups: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}` + }); + } + + if (error instanceof InternalServerError) { + throw error; + } + + throw new InternalServerError({ + message: "Unable to fetch GitLab groups" + }); + } +}; diff --git a/backend/src/services/app-connection/gitlab/gitlab-connection-schemas.ts b/backend/src/services/app-connection/gitlab/gitlab-connection-schemas.ts new file mode 100644 index 000000000..936a370bf --- /dev/null +++ b/backend/src/services/app-connection/gitlab/gitlab-connection-schemas.ts @@ -0,0 +1,138 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { GitLabAccessTokenType, GitLabConnectionMethod } from "./gitlab-connection-enums"; + +export const GitLabConnectionAccessTokenCredentialsSchema = z.object({ + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .describe(AppConnections.CREDENTIALS.GITLAB.accessToken), + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .optional() + .describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl), + accessTokenType: z.nativeEnum(GitLabAccessTokenType).describe(AppConnections.CREDENTIALS.GITLAB.accessTokenType) +}); + +export const GitLabConnectionOAuthCredentialsSchema = z.object({ + code: z.string().trim().min(1, "OAuth code required").describe(AppConnections.CREDENTIALS.GITLAB.code), + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .optional() + .describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl) +}); + +export const GitLabConnectionOAuthOutputCredentialsSchema = z.object({ + accessToken: z.string().trim(), + refreshToken: z.string().trim(), + expiresAt: z.date(), + tokenType: z.string().optional().default("bearer"), + createdAt: z.string().optional(), + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .optional() + .describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl) +}); + +export const GitLabConnectionRefreshTokenCredentialsSchema = z.object({ + refreshToken: z.string().trim().min(1, "Refresh token required"), + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .optional() + .describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl) +}); + +const BaseGitLabConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.GitLab) +}); + +export const GitLabConnectionSchema = z.intersection( + BaseGitLabConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(GitLabConnectionMethod.AccessToken), + credentials: GitLabConnectionAccessTokenCredentialsSchema + }), + z.object({ + method: z.literal(GitLabConnectionMethod.OAuth), + credentials: GitLabConnectionOAuthOutputCredentialsSchema + }) + ]) +); + +export const SanitizedGitLabConnectionSchema = z.discriminatedUnion("method", [ + BaseGitLabConnectionSchema.extend({ + method: z.literal(GitLabConnectionMethod.AccessToken), + credentials: GitLabConnectionAccessTokenCredentialsSchema.pick({ + instanceUrl: true, + accessTokenType: true + }) + }), + BaseGitLabConnectionSchema.extend({ + method: z.literal(GitLabConnectionMethod.OAuth), + credentials: GitLabConnectionOAuthOutputCredentialsSchema.pick({ + instanceUrl: true + }) + }) +]); + +export const ValidateGitLabConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(GitLabConnectionMethod.AccessToken).describe(AppConnections.CREATE(AppConnection.GitLab).method), + credentials: GitLabConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.GitLab).credentials + ) + }), + z.object({ + method: z.literal(GitLabConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitLab).method), + credentials: z + .union([ + GitLabConnectionOAuthCredentialsSchema, + GitLabConnectionRefreshTokenCredentialsSchema, + GitLabConnectionOAuthOutputCredentialsSchema + ]) + .describe(AppConnections.CREATE(AppConnection.GitLab).credentials) + }) +]); + +export const CreateGitLabConnectionSchema = ValidateGitLabConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.GitLab) +); + +export const UpdateGitLabConnectionSchema = z + .object({ + credentials: z + .union([ + GitLabConnectionAccessTokenCredentialsSchema, + GitLabConnectionOAuthOutputCredentialsSchema, + GitLabConnectionRefreshTokenCredentialsSchema, + GitLabConnectionOAuthCredentialsSchema + ]) + .optional() + .describe(AppConnections.UPDATE(AppConnection.GitLab).credentials) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitLab)); + +export const GitLabConnectionListItemSchema = z.object({ + name: z.literal("GitLab"), + app: z.literal(AppConnection.GitLab), + methods: z.nativeEnum(GitLabConnectionMethod).array(), + oauthClientId: z.string().optional() +}); diff --git a/backend/src/services/app-connection/gitlab/gitlab-connection-service.ts b/backend/src/services/app-connection/gitlab/gitlab-connection-service.ts new file mode 100644 index 000000000..818697be3 --- /dev/null +++ b/backend/src/services/app-connection/gitlab/gitlab-connection-service.ts @@ -0,0 +1,47 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAppConnectionDALFactory } from "../app-connection-dal"; +import { AppConnection } from "../app-connection-enums"; +import { listGitLabGroups, listGitLabProjects } from "./gitlab-connection-fns"; +import { TGitLabConnection } from "./gitlab-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const gitlabConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listProjects = async (connectionId: string, actor: OrgServiceActor) => { + try { + const appConnection = await getAppConnection(AppConnection.GitLab, connectionId, actor); + const projects = await listGitLabProjects({ appConnection, appConnectionDAL, kmsService }); + return projects; + } catch (error) { + logger.error(error, `Failed to establish connection with GitLab for app ${connectionId}`); + return []; + } + }; + + const listGroups = async (connectionId: string, actor: OrgServiceActor) => { + try { + const appConnection = await getAppConnection(AppConnection.GitLab, connectionId, actor); + const groups = await listGitLabGroups({ appConnection, appConnectionDAL, kmsService }); + return groups; + } catch (error) { + logger.error(error, `Failed to establish connection with GitLab for app ${connectionId}`); + return []; + } + }; + + return { + listProjects, + listGroups + }; +}; diff --git a/backend/src/services/app-connection/gitlab/gitlab-connection-types.ts b/backend/src/services/app-connection/gitlab/gitlab-connection-types.ts new file mode 100644 index 000000000..73c3d7a46 --- /dev/null +++ b/backend/src/services/app-connection/gitlab/gitlab-connection-types.ts @@ -0,0 +1,56 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateGitLabConnectionSchema, + GitLabConnectionSchema, + ValidateGitLabConnectionCredentialsSchema +} from "./gitlab-connection-schemas"; + +export type TGitLabConnection = z.infer; + +export type TGitLabConnectionInput = z.infer & { + app: AppConnection.GitLab; +}; + +export type TValidateGitLabConnectionCredentialsSchema = typeof ValidateGitLabConnectionCredentialsSchema; + +export type TGitLabConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TGitLabProject = { + name: string; + id: string; +}; + +export type TGitLabAccessTokenCredentials = { + accessToken: string; + instanceUrl: string; +}; + +export type TGitLabOAuthCredentials = { + accessToken: string; + refreshToken: string; + expiresAt: Date; + tokenType?: string; + createdAt?: Date; + instanceUrl: string; +}; + +export type TGitLabOAuthCodeCredentials = { + code: string; + instanceUrl: string; +}; + +export type TGitLabRefreshTokenCredentials = { + refreshToken: string; + instanceUrl: string; +}; + +export interface TGitLabGroup { + id: string; + name: string; +} diff --git a/backend/src/services/app-connection/gitlab/index.ts b/backend/src/services/app-connection/gitlab/index.ts new file mode 100644 index 000000000..9c6463d8c --- /dev/null +++ b/backend/src/services/app-connection/gitlab/index.ts @@ -0,0 +1,4 @@ +export * from "./gitlab-connection-enums"; +export * from "./gitlab-connection-fns"; +export * from "./gitlab-connection-schemas"; +export * from "./gitlab-connection-types"; diff --git a/backend/src/services/secret-sync/gitlab/gitlab-sync-constants.ts b/backend/src/services/secret-sync/gitlab/gitlab-sync-constants.ts new file mode 100644 index 000000000..95ea1b424 --- /dev/null +++ b/backend/src/services/secret-sync/gitlab/gitlab-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const GITLAB_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "GitLab", + destination: SecretSync.GitLab, + connection: AppConnection.GitLab, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/gitlab/gitlab-sync-enums.ts b/backend/src/services/secret-sync/gitlab/gitlab-sync-enums.ts new file mode 100644 index 000000000..9f735f563 --- /dev/null +++ b/backend/src/services/secret-sync/gitlab/gitlab-sync-enums.ts @@ -0,0 +1,4 @@ +export enum GitLabSyncScope { + Project = "project", + Group = "group" +} diff --git a/backend/src/services/secret-sync/gitlab/gitlab-sync-fns.ts b/backend/src/services/secret-sync/gitlab/gitlab-sync-fns.ts new file mode 100644 index 000000000..3c152d853 --- /dev/null +++ b/backend/src/services/secret-sync/gitlab/gitlab-sync-fns.ts @@ -0,0 +1,452 @@ +/* eslint-disable no-await-in-loop */ +import { GitbeakerRequestError } from "@gitbeaker/rest"; + +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { + getGitLabClient, + GitLabConnectionMethod, + refreshGitLabToken, + TGitLabConnection +} from "@app/services/app-connection/gitlab"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TGitLabSyncWithCredentials, TGitLabVariable } from "@app/services/secret-sync/gitlab/gitlab-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; +import { GitLabSyncScope } from "./gitlab-sync-enums"; + +interface TGitLabVariablePayload { + key?: string; + value: string; + variable_type?: "env_var" | "file"; + environment_scope?: string; + protected?: boolean; + masked?: boolean; + masked_and_hidden?: boolean; + description?: string; +} + +interface TGitLabVariableCreate extends TGitLabVariablePayload { + key: string; +} + +interface TGitLabVariableUpdate extends Omit {} + +type TGitLabSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +const getValidAccessToken = async ( + connection: TGitLabConnection, + appConnectionDAL: Pick, + kmsService: Pick +): Promise => { + if ( + connection.method === GitLabConnectionMethod.OAuth && + connection.credentials.refreshToken && + new Date(connection.credentials.expiresAt) < new Date() + ) { + const accessToken = await refreshGitLabToken( + connection.credentials.refreshToken, + connection.id, + connection.orgId, + appConnectionDAL, + kmsService, + connection.credentials.instanceUrl + ); + return accessToken; + } + return connection.credentials.accessToken; +}; + +const getGitLabVariables = async ({ + accessToken, + connection, + scope, + resourceId, + targetEnvironment +}: { + accessToken: string; + connection: TGitLabConnection; + scope: GitLabSyncScope; + resourceId: string; + targetEnvironment?: string; +}): Promise => { + try { + const client = await getGitLabClient( + accessToken, + connection.credentials.instanceUrl, + connection.method === GitLabConnectionMethod.OAuth + ); + + let variables: TGitLabVariable[] = []; + + if (scope === GitLabSyncScope.Project) { + variables = await client.ProjectVariables.all(resourceId); + } else { + variables = await client.GroupVariables.all(resourceId); + } + + if (targetEnvironment) { + variables = variables.filter((v) => v.environmentScope === targetEnvironment); + } + + return variables; + } catch (error) { + if (error instanceof GitbeakerRequestError) { + throw new SecretSyncError({ + error: new Error( + `Failed to fetch variables: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}` + ) + }); + } + throw new SecretSyncError({ + error + }); + } +}; + +const createGitLabVariable = async ({ + accessToken, + connection, + scope, + resourceId, + variable +}: { + accessToken: string; + connection: TGitLabConnection; + scope: GitLabSyncScope; + resourceId: string; + variable: TGitLabVariableCreate; +}): Promise => { + try { + const client = await getGitLabClient( + accessToken, + connection.credentials.instanceUrl, + connection.method === GitLabConnectionMethod.OAuth + ); + + const payload = { + key: variable.key, + value: variable.value, + variableType: "env_var", + environmentScope: variable.environment_scope || "*", + protected: variable.protected || false, + masked: variable.masked || false, + masked_and_hidden: variable.masked_and_hidden || false, + raw: false + }; + + if (scope === GitLabSyncScope.Project) { + await client.ProjectVariables.create(resourceId, payload.key, payload.value, { + variableType: "env_var", + environmentScope: payload.environmentScope, + protected: payload.protected, + masked: payload.masked, + masked_and_hidden: payload.masked_and_hidden, + raw: false + }); + } else { + await client.GroupVariables.create(resourceId, payload.key, payload.value, { + variableType: "env_var", + environmentScope: payload.environmentScope, + protected: payload.protected, + masked: payload.masked, + ...(payload.masked_and_hidden && { masked_and_hidden: payload.masked_and_hidden }), + raw: false + }); + } + } catch (error) { + if (error instanceof GitbeakerRequestError) { + throw new SecretSyncError({ + error: new Error( + `Failed to create variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}` + ), + secretKey: variable.key + }); + } + throw new SecretSyncError({ + error, + secretKey: variable.key + }); + } +}; + +const updateGitLabVariable = async ({ + accessToken, + connection, + scope, + resourceId, + key, + variable, + targetEnvironment +}: { + accessToken: string; + connection: TGitLabConnection; + scope: GitLabSyncScope; + resourceId: string; + key: string; + variable: TGitLabVariableUpdate; + targetEnvironment?: string; +}): Promise => { + try { + const client = await getGitLabClient( + accessToken, + connection.credentials.instanceUrl, + connection.method === GitLabConnectionMethod.OAuth + ); + + const options = { + ...(variable.environment_scope && { environmentScope: variable.environment_scope }), + ...(variable.protected !== undefined && { protected: variable.protected }), + ...(variable.masked !== undefined && { masked: variable.masked }) + }; + + if (targetEnvironment) { + options.environmentScope = targetEnvironment; + } + + if (scope === GitLabSyncScope.Project) { + await client.ProjectVariables.edit(resourceId, key, variable.value, { + ...options, + filter: { environment_scope: targetEnvironment || "*" } + }); + } else { + await client.GroupVariables.edit(resourceId, key, variable.value, { + ...options, + filter: { environment_scope: targetEnvironment || "*" } + }); + } + } catch (error) { + if (error instanceof GitbeakerRequestError) { + throw new SecretSyncError({ + error: new Error( + `Failed to update variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}` + ), + secretKey: key + }); + } + throw new SecretSyncError({ + error, + secretKey: key + }); + } +}; + +const deleteGitLabVariable = async ({ + accessToken, + connection, + scope, + resourceId, + key, + targetEnvironment, + allVariables +}: { + accessToken: string; + connection: TGitLabConnection; + scope: GitLabSyncScope; + resourceId: string; + key: string; + targetEnvironment?: string; + allVariables?: TGitLabVariable[]; +}): Promise => { + if (allVariables && !allVariables.find((v) => v.key === key)) { + return; + } + try { + const client = await getGitLabClient( + accessToken, + connection.credentials.instanceUrl, + connection.method === GitLabConnectionMethod.OAuth + ); + + const options: { filter?: { environment_scope: string } } = {}; + if (targetEnvironment) { + options.filter = { environment_scope: targetEnvironment || "*" }; + } + + if (scope === GitLabSyncScope.Project) { + await client.ProjectVariables.remove(resourceId, key, options); + } else { + await client.GroupVariables.remove(resourceId, key); + } + } catch (error: unknown) { + if (error instanceof GitbeakerRequestError) { + throw new SecretSyncError({ + error: new Error( + `Failed to delete variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}` + ), + secretKey: key + }); + } + throw new SecretSyncError({ + error, + secretKey: key + }); + } +}; + +export const GitLabSyncFns = { + syncSecrets: async ( + secretSync: TGitLabSyncWithCredentials, + secretMap: TSecretMap, + { appConnectionDAL, kmsService }: TGitLabSyncFactoryDeps + ): Promise => { + const { connection, environment, destinationConfig } = secretSync; + const { scope, targetEnvironment } = destinationConfig; + + const resourceId = scope === GitLabSyncScope.Project ? destinationConfig.projectId : destinationConfig.groupId; + + const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService); + + try { + const currentVariables = await getGitLabVariables({ + accessToken, + connection, + scope, + resourceId, + targetEnvironment + }); + + const currentVariableMap = new Map(currentVariables.map((v) => [v.key, v])); + + for (const [key, { value }] of Object.entries(secretMap)) { + if (value?.length < 8 && destinationConfig.shouldMaskSecrets) { + throw new SecretSyncError({ + message: `Secret ${key} is too short to be masked. GitLab requires a minimum of 8 characters for masked secrets.`, + secretKey: key + }); + } + try { + const existingVariable = currentVariableMap.get(key); + + if (existingVariable) { + if ( + existingVariable.value !== value || + existingVariable.environmentScope !== targetEnvironment || + existingVariable.protected !== destinationConfig.shouldProtectSecrets || + existingVariable.masked !== destinationConfig.shouldMaskSecrets + ) { + await updateGitLabVariable({ + accessToken, + connection, + scope, + resourceId, + key, + variable: { + value, + environment_scope: targetEnvironment, + protected: destinationConfig.shouldProtectSecrets, + masked: destinationConfig.shouldMaskSecrets || existingVariable.hidden + }, + targetEnvironment + }); + } + } else { + await createGitLabVariable({ + accessToken, + connection, + scope, + resourceId, + variable: { + key, + value, + variable_type: "env_var", + environment_scope: targetEnvironment || "*", + protected: destinationConfig.shouldProtectSecrets || false, + masked: destinationConfig.shouldMaskSecrets || false, + masked_and_hidden: destinationConfig.shouldHideSecrets || false + } + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (!secretSync.syncOptions.disableSecretDeletion) { + for (const variable of currentVariables) { + try { + const shouldDelete = + matchesSchema(variable.key, environment?.slug || "", secretSync.syncOptions.keySchema) && + !(variable.key in secretMap); + + if (shouldDelete) { + await deleteGitLabVariable({ + accessToken, + connection, + scope, + resourceId, + key: variable.key, + targetEnvironment + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: variable.key + }); + } + } + } + } catch (error) { + if (error instanceof SecretSyncError) { + throw error; + } + throw new SecretSyncError({ + message: "Failed to sync secrets", + error + }); + } + }, + + removeSecrets: async ( + secretSync: TGitLabSyncWithCredentials, + secretMap: TSecretMap, + { appConnectionDAL, kmsService }: TGitLabSyncFactoryDeps + ): Promise => { + const { connection, destinationConfig } = secretSync; + const { scope, targetEnvironment } = destinationConfig; + + const resourceId = scope === GitLabSyncScope.Project ? destinationConfig.projectId : destinationConfig.groupId; + + const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService); + + const allVariables = await getGitLabVariables({ + accessToken, + connection, + scope, + resourceId, + targetEnvironment + }); + + for (const key of Object.keys(secretMap)) { + try { + await deleteGitLabVariable({ + accessToken, + connection, + scope, + resourceId, + key, + targetEnvironment, + allVariables + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + }, + + getSecrets: async (secretSync: TGitLabSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + } +}; diff --git a/backend/src/services/secret-sync/gitlab/gitlab-sync-schemas.ts b/backend/src/services/secret-sync/gitlab/gitlab-sync-schemas.ts new file mode 100644 index 000000000..8797cc98d --- /dev/null +++ b/backend/src/services/secret-sync/gitlab/gitlab-sync-schemas.ts @@ -0,0 +1,97 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +import { GitLabSyncScope } from "./gitlab-sync-enums"; + +const GitLabSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(GitLabSyncScope.Project).describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.scope), + projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.projectId), + projectName: z + .string() + .min(1, "Project name is required") + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.projectName), + targetEnvironment: z + .string() + .optional() + .default("*") + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.targetEnvironment), + shouldProtectSecrets: z + .boolean() + .optional() + .default(false) + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldProtectSecrets), + shouldMaskSecrets: z + .boolean() + .optional() + .default(false) + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldMaskSecrets), + shouldHideSecrets: z + .boolean() + .optional() + .default(false) + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldHideSecrets) + }), + z.object({ + scope: z.literal(GitLabSyncScope.Group).describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.scope), + groupId: z.string().min(1, "Group ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.groupId), + groupName: z.string().min(1, "Group name is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.groupName), + targetEnvironment: z + .string() + .optional() + .default("*") + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.targetEnvironment), + shouldProtectSecrets: z + .boolean() + .optional() + .default(false) + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldProtectSecrets), + shouldMaskSecrets: z + .boolean() + .optional() + .default(false) + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldMaskSecrets), + shouldHideSecrets: z + .boolean() + .optional() + .default(false) + .describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldHideSecrets) + }) +]); + +const GitLabSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const GitLabSyncSchema = BaseSecretSyncSchema(SecretSync.GitLab, GitLabSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.GitLab), + destinationConfig: GitLabSyncDestinationConfigSchema +}); + +export const CreateGitLabSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.GitLab, + GitLabSyncOptionsConfig +).extend({ + destinationConfig: GitLabSyncDestinationConfigSchema +}); + +export const UpdateGitLabSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.GitLab, + GitLabSyncOptionsConfig +).extend({ + destinationConfig: GitLabSyncDestinationConfigSchema.optional() +}); + +export const GitLabSyncListItemSchema = z.object({ + name: z.literal("GitLab"), + connection: z.literal(AppConnection.GitLab), + destination: z.literal(SecretSync.GitLab), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/gitlab/gitlab-sync-types.ts b/backend/src/services/secret-sync/gitlab/gitlab-sync-types.ts new file mode 100644 index 000000000..7a52a4126 --- /dev/null +++ b/backend/src/services/secret-sync/gitlab/gitlab-sync-types.ts @@ -0,0 +1,58 @@ +import { z } from "zod"; + +import { TGitLabConnection } from "@app/services/app-connection/gitlab"; + +import { CreateGitLabSyncSchema, GitLabSyncListItemSchema, GitLabSyncSchema } from "./gitlab-sync-schemas"; + +export type TGitLabSync = z.infer; +export type TGitLabSyncInput = z.infer; +export type TGitLabSyncListItem = z.infer; + +export type TGitLabSyncWithCredentials = TGitLabSync & { + connection: TGitLabConnection; +}; + +export type TGitLabVariable = { + key: string; + value: string; + protected: boolean; + masked: boolean; + environmentScope?: string; + hidden?: boolean; +}; + +export type TGitLabVariableCreate = { + key: string; + value: string; + variable_type?: "env_var" | "file"; + protected?: boolean; + masked?: boolean; + raw?: boolean; + environment_scope?: string; + description?: string; +}; + +export type TGitLabVariableUpdate = { + value: string; + variable_type?: "env_var" | "file"; + protected?: boolean; + masked?: boolean; + raw?: boolean; + environment_scope?: string; + description?: string | null; +}; + +export type TGitLabListVariables = { + accessToken: string; + projectId: string; + environmentScope?: string; +}; + +export type TGitLabCreateVariable = TGitLabListVariables & { + variable: TGitLabVariableCreate; +}; + +export type TGitLabUpdateVariable = TGitLabListVariables & { + key: string; + variable: TGitLabVariableUpdate; +}; diff --git a/backend/src/services/secret-sync/gitlab/index.ts b/backend/src/services/secret-sync/gitlab/index.ts new file mode 100644 index 000000000..5072b77f1 --- /dev/null +++ b/backend/src/services/secret-sync/gitlab/index.ts @@ -0,0 +1,4 @@ +export * from "./gitlab-sync-constants"; +export * from "./gitlab-sync-fns"; +export * from "./gitlab-sync-schemas"; +export * from "./gitlab-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 1c13f85bd..b70b37caf 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -19,6 +19,7 @@ export enum SecretSync { Heroku = "heroku", Render = "render", Flyio = "flyio", + GitLab = "gitlab", CloudflarePages = "cloudflare-pages" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 05ebe05ed..9d0513a2c 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -34,6 +34,7 @@ import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns" import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; +import { GITLAB_SYNC_LIST_OPTION, GitLabSyncFns } from "./gitlab"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; @@ -66,6 +67,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION, [SecretSync.Render]: RENDER_SYNC_LIST_OPTION, [SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION, + [SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION, [SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION }; @@ -230,6 +232,8 @@ export const SecretSyncFns = { return RenderSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Flyio: return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.GitLab: + return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); case SecretSync.CloudflarePages: return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap); default: @@ -318,6 +322,9 @@ export const SecretSyncFns = { case SecretSync.Flyio: secretMap = await FlyioSyncFns.getSecrets(secretSync); break; + case SecretSync.GitLab: + secretMap = await GitLabSyncFns.getSecrets(secretSync); + break; case SecretSync.CloudflarePages: secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync); break; @@ -394,6 +401,8 @@ export const SecretSyncFns = { return RenderSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Flyio: return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.GitLab: + return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); case SecretSync.CloudflarePages: return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap); default: diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 2e93beed9..1dc0ea6c0 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -22,6 +22,7 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Heroku]: "Heroku", [SecretSync.Render]: "Render", [SecretSync.Flyio]: "Fly.io", + [SecretSync.GitLab]: "GitLab", [SecretSync.CloudflarePages]: "Cloudflare Pages" }; @@ -46,6 +47,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Heroku]: AppConnection.Heroku, [SecretSync.Render]: AppConnection.Render, [SecretSync.Flyio]: AppConnection.Flyio, + [SecretSync.GitLab]: AppConnection.GitLab, [SecretSync.CloudflarePages]: AppConnection.Cloudflare }; @@ -70,5 +72,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Heroku]: SecretSyncPlanType.Regular, [SecretSync.Render]: SecretSyncPlanType.Regular, [SecretSync.Flyio]: SecretSyncPlanType.Regular, + [SecretSync.GitLab]: SecretSyncPlanType.Regular, [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 51a64a708..a31183280 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -72,8 +72,15 @@ import { TAzureKeyVaultSyncListItem, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; +import { + TCloudflarePagesSync, + TCloudflarePagesSyncInput, + TCloudflarePagesSyncListItem, + TCloudflarePagesSyncWithCredentials +} from "./cloudflare-pages/cloudflare-pages-types"; import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; +import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab"; import { THCVaultSync, THCVaultSyncInput, @@ -106,12 +113,6 @@ import { TTerraformCloudSyncWithCredentials } from "./terraform-cloud"; import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel"; -import { - TCloudflarePagesSync, - TCloudflarePagesSyncInput, - TCloudflarePagesSyncListItem, - TCloudflarePagesSyncWithCredentials -} from "./cloudflare-pages/cloudflare-pages-types"; export type TSecretSync = | TAwsParameterStoreSync @@ -134,6 +135,7 @@ export type TSecretSync = | THerokuSync | TRenderSync | TFlyioSync + | TGitLabSync | TCloudflarePagesSync; export type TSecretSyncWithCredentials = @@ -157,6 +159,7 @@ export type TSecretSyncWithCredentials = | THerokuSyncWithCredentials | TRenderSyncWithCredentials | TFlyioSyncWithCredentials + | TGitLabSyncWithCredentials | TCloudflarePagesSyncWithCredentials; export type TSecretSyncInput = @@ -180,6 +183,7 @@ export type TSecretSyncInput = | THerokuSyncInput | TRenderSyncInput | TFlyioSyncInput + | TGitLabSyncInput | TCloudflarePagesSyncInput; export type TSecretSyncListItem = @@ -203,6 +207,7 @@ export type TSecretSyncListItem = | THerokuSyncListItem | TRenderSyncListItem | TFlyioSyncListItem + | TGitLabSyncListItem | TCloudflarePagesSyncListItem; export type TSyncOptionsConfig = { diff --git a/docs/api-reference/endpoints/app-connections/gitlab/available.mdx b/docs/api-reference/endpoints/app-connections/gitlab/available.mdx new file mode 100644 index 000000000..155d8e98e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gitlab/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/gitlab/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/gitlab/create.mdx b/docs/api-reference/endpoints/app-connections/gitlab/create.mdx new file mode 100644 index 000000000..3acab1ea9 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gitlab/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/gitlab" +--- + + + Gitlab OAuth Connections must be created through the Infisical UI. + Check out the configuration docs for [Gitlab OAuth Connections](/integrations/app-connections/gitlab) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/gitlab/delete.mdx b/docs/api-reference/endpoints/app-connections/gitlab/delete.mdx new file mode 100644 index 000000000..d5b32edba --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gitlab/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/gitlab/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gitlab/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/gitlab/get-by-id.mdx new file mode 100644 index 000000000..b581efa22 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gitlab/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/gitlab/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gitlab/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/gitlab/get-by-name.mdx new file mode 100644 index 000000000..32da4f6ad --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gitlab/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/gitlab/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gitlab/list.mdx b/docs/api-reference/endpoints/app-connections/gitlab/list.mdx new file mode 100644 index 000000000..d1fc14563 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gitlab/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/gitlab" +--- diff --git a/docs/api-reference/endpoints/app-connections/gitlab/update.mdx b/docs/api-reference/endpoints/app-connections/gitlab/update.mdx new file mode 100644 index 000000000..16162ad8f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gitlab/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/gitlab/{connectionId}" +--- + + + Gitlab OAuth Connections must be updated through the Infisical UI. + Check out the configuration docs for [Gitlab OAuth Connections](/integrations/app-connections/gitlab) for a step-by-step + guide. + diff --git a/docs/api-reference/endpoints/secret-syncs/gitlab/create.mdx b/docs/api-reference/endpoints/secret-syncs/gitlab/create.mdx new file mode 100644 index 000000000..179655883 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gitlab/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/gitlab" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gitlab/delete.mdx b/docs/api-reference/endpoints/secret-syncs/gitlab/delete.mdx new file mode 100644 index 000000000..57b036a7d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gitlab/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/gitlab/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gitlab/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/gitlab/get-by-id.mdx new file mode 100644 index 000000000..2737afca4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gitlab/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/gitlab/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gitlab/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/gitlab/get-by-name.mdx new file mode 100644 index 000000000..a17a27a9b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gitlab/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/gitlab/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gitlab/list.mdx b/docs/api-reference/endpoints/secret-syncs/gitlab/list.mdx new file mode 100644 index 000000000..d9f6ac63b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gitlab/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/gitlab" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gitlab/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/gitlab/remove-secrets.mdx new file mode 100644 index 000000000..bf67639f3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gitlab/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/gitlab/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gitlab/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/gitlab/sync-secrets.mdx new file mode 100644 index 000000000..7fc46593f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gitlab/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/gitlab/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gitlab/update.mdx b/docs/api-reference/endpoints/secret-syncs/gitlab/update.mdx new file mode 100644 index 000000000..1cb5658a1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gitlab/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/gitlab/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index a96ebc5d0..e0d0db73a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -475,6 +475,7 @@ "integrations/app-connections/gcp", "integrations/app-connections/github", "integrations/app-connections/github-radar", + "integrations/app-connections/gitlab", "integrations/app-connections/hashicorp-vault", "integrations/app-connections/heroku", "integrations/app-connections/humanitec", @@ -512,6 +513,7 @@ "integrations/secret-syncs/flyio", "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", + "integrations/secret-syncs/gitlab", "integrations/secret-syncs/hashicorp-vault", "integrations/secret-syncs/heroku", "integrations/secret-syncs/humanitec", @@ -1317,6 +1319,18 @@ "api-reference/endpoints/app-connections/github/delete" ] }, + { + "group": "GitLab", + "pages": [ + "api-reference/endpoints/app-connections/gitlab/list", + "api-reference/endpoints/app-connections/gitlab/available", + "api-reference/endpoints/app-connections/gitlab/get-by-id", + "api-reference/endpoints/app-connections/gitlab/get-by-name", + "api-reference/endpoints/app-connections/gitlab/create", + "api-reference/endpoints/app-connections/gitlab/update", + "api-reference/endpoints/app-connections/gitlab/delete" + ] + }, { "group": "GitHub Radar", "pages": [ @@ -1667,6 +1681,19 @@ "api-reference/endpoints/secret-syncs/github/remove-secrets" ] }, + { + "group": "GitLab", + "pages": [ + "api-reference/endpoints/secret-syncs/gitlab/list", + "api-reference/endpoints/secret-syncs/gitlab/get-by-id", + "api-reference/endpoints/secret-syncs/gitlab/get-by-name", + "api-reference/endpoints/secret-syncs/gitlab/create", + "api-reference/endpoints/secret-syncs/gitlab/update", + "api-reference/endpoints/secret-syncs/gitlab/delete", + "api-reference/endpoints/secret-syncs/gitlab/sync-secrets", + "api-reference/endpoints/secret-syncs/gitlab/remove-secrets" + ] + }, { "group": "Hashicorp Vault", "pages": [ diff --git a/docs/images/app-connections/gitlab/create-gitlab-access-token-connection.png b/docs/images/app-connections/gitlab/create-gitlab-access-token-connection.png index 379d6e0e1..0a7ba027f 100644 Binary files a/docs/images/app-connections/gitlab/create-gitlab-access-token-connection.png and b/docs/images/app-connections/gitlab/create-gitlab-access-token-connection.png differ diff --git a/docs/images/app-connections/gitlab/create-gitlab-oauth-connection.png b/docs/images/app-connections/gitlab/create-gitlab-oauth-connection.png new file mode 100644 index 000000000..f0df047fa Binary files /dev/null and b/docs/images/app-connections/gitlab/create-gitlab-oauth-connection.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-access-token-connection-created.png b/docs/images/app-connections/gitlab/gitlab-access-token-connection-created.png deleted file mode 100644 index 85da8a929..000000000 Binary files a/docs/images/app-connections/gitlab/gitlab-access-token-connection-created.png and /dev/null differ diff --git a/docs/images/app-connections/gitlab/gitlab-access-token-connection.png b/docs/images/app-connections/gitlab/gitlab-access-token-connection.png new file mode 100644 index 000000000..70a1dfb50 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-access-token-connection.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-add-access-token.png b/docs/images/app-connections/gitlab/gitlab-add-access-token.png index b1307f774..73073b61c 100644 Binary files a/docs/images/app-connections/gitlab/gitlab-add-access-token.png and b/docs/images/app-connections/gitlab/gitlab-add-access-token.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-applications.png b/docs/images/app-connections/gitlab/gitlab-applications.png new file mode 100644 index 000000000..0e69b42b2 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-applications.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-authorization-page.png b/docs/images/app-connections/gitlab/gitlab-authorization-page.png new file mode 100644 index 000000000..298d5a7ec Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-authorization-page.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-config-credentials.png b/docs/images/app-connections/gitlab/gitlab-config-credentials.png new file mode 100644 index 000000000..028b20bbc Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-config-credentials.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-copy-token.png b/docs/images/app-connections/gitlab/gitlab-copy-token.png index e425ac3ef..9d5b56977 100644 Binary files a/docs/images/app-connections/gitlab/gitlab-copy-token.png and b/docs/images/app-connections/gitlab/gitlab-copy-token.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-create-application-bottom.png b/docs/images/app-connections/gitlab/gitlab-create-application-bottom.png new file mode 100644 index 000000000..1be784b08 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-create-application-bottom.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-create-application-top.png b/docs/images/app-connections/gitlab/gitlab-create-application-top.png new file mode 100644 index 000000000..676c2f203 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-create-application-top.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-dashboard.png b/docs/images/app-connections/gitlab/gitlab-dashboard.png new file mode 100644 index 000000000..ee61cccc6 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-dashboard.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-oauth-connection.png b/docs/images/app-connections/gitlab/gitlab-oauth-connection.png new file mode 100644 index 000000000..972c69d20 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-oauth-connection.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-personal-access-token-form.png b/docs/images/app-connections/gitlab/gitlab-personal-access-token-form.png new file mode 100644 index 000000000..87b140b15 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-personal-access-token-form.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-project-access-token-created.png b/docs/images/app-connections/gitlab/gitlab-project-access-token-created.png new file mode 100644 index 000000000..09de4c027 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-project-access-token-created.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-project-access-token-form.png b/docs/images/app-connections/gitlab/gitlab-project-access-token-form.png new file mode 100644 index 000000000..c6f9593cb Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-project-access-token-form.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-project-access-token-list.png b/docs/images/app-connections/gitlab/gitlab-project-access-token-list.png new file mode 100644 index 000000000..72499d0a1 Binary files /dev/null and b/docs/images/app-connections/gitlab/gitlab-project-access-token-list.png differ diff --git a/docs/images/app-connections/gitlab/gitlab-secret-scanning-token.png b/docs/images/app-connections/gitlab/gitlab-secret-scanning-token.png deleted file mode 100644 index 69575afe7..000000000 Binary files a/docs/images/app-connections/gitlab/gitlab-secret-scanning-token.png and /dev/null differ diff --git a/docs/images/secret-syncs/gitlab/gitlab-secret-sync-created.png b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-created.png new file mode 100644 index 000000000..6588f443d Binary files /dev/null and b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-created.png differ diff --git a/docs/images/secret-syncs/gitlab/gitlab-secret-sync-destination.png b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-destination.png new file mode 100644 index 000000000..a8b1ab6bc Binary files /dev/null and b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-destination.png differ diff --git a/docs/images/secret-syncs/gitlab/gitlab-secret-sync-details.png b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-details.png new file mode 100644 index 000000000..93717b659 Binary files /dev/null and b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-details.png differ diff --git a/docs/images/secret-syncs/gitlab/gitlab-secret-sync-option.png b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-option.png new file mode 100644 index 000000000..658e5a5d0 Binary files /dev/null and b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-option.png differ diff --git a/docs/images/secret-syncs/gitlab/gitlab-secret-sync-options.png b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-options.png new file mode 100644 index 000000000..cd897816d Binary files /dev/null and b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-options.png differ diff --git a/docs/images/secret-syncs/gitlab/gitlab-secret-sync-review.png b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-review.png new file mode 100644 index 000000000..b389aa686 Binary files /dev/null and b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-review.png differ diff --git a/docs/images/secret-syncs/gitlab/gitlab-secret-sync-source.png b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-source.png new file mode 100644 index 000000000..cc0a5e72f Binary files /dev/null and b/docs/images/secret-syncs/gitlab/gitlab-secret-sync-source.png differ diff --git a/docs/integrations/app-connections/gitlab.mdx b/docs/integrations/app-connections/gitlab.mdx new file mode 100644 index 000000000..b81c0fbcd --- /dev/null +++ b/docs/integrations/app-connections/gitlab.mdx @@ -0,0 +1,192 @@ +--- +title: "GitLab Connection" +description: "Learn how to configure a GitLab Connection for Infisical using OAuth or Access Token methods." +--- + +Infisical supports two methods for connecting to GitLab: **OAuth** and **Access Token**. Choose the method that best fits your setup and security requirements. + + + + The OAuth method provides secure authentication through GitLab's OAuth flow. + + + Using the GitLab Connection with OAuth on a self-hosted instance of Infisical requires configuring an OAuth application in GitLab and registering your instance with it. + + **Prerequisites:** + - A GitLab account with existing projects + - Self-hosted Infisical instance + + + + Navigate to your user Settings > Applications to create a new GitLab application. + + ![GitLab Dashboard](/images/app-connections/gitlab/gitlab-dashboard.png) + ![GitLab Applications Settings](/images/app-connections/gitlab/gitlab-applications.png) + + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/gitlab/oauth/callback`. + + ![GitLab New Application Form](/images/app-connections/gitlab/gitlab-create-application-top.png) + ![GitLab New Application Form](/images/app-connections/gitlab/gitlab-create-application-bottom.png) + + + The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance. + + + + If you have a GitLab group, you can create an OAuth application under it in your group Settings > Applications. + + + + Obtain the **Application ID** and **Secret** for your GitLab OAuth application. + + ![GitLab Application Credentials](/images/app-connections/gitlab/gitlab-config-credentials.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your GitLab OAuth application: + + - `INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID`: The **Application ID** of your GitLab OAuth application. + - `INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET`: The **Secret** of your GitLab OAuth application. + + Once added, restart your Infisical instance and use the GitLab Connection. + + + + + ## Setup GitLab OAuth Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **GitLab Connection** option from the connection options modal. + ![Select GitLab Connection](/images/app-connections/gitlab/select-gitlab-connection.png) + + + Select the **OAuth** method and click **Connect to GitLab**. + + ![Connect via GitLab OAuth](/images/app-connections/gitlab/create-gitlab-oauth-connection.png) + + + You will be redirected to GitLab to grant Infisical access to your GitLab account. Once granted, you will be redirected back to Infisical's App Connections page. + ![GitLab Authorization](/images/app-connections/gitlab/gitlab-authorization-page.png) + + + Your **GitLab Connection** is now available for use. + ![GitLab OAuth Connection](/images/app-connections/gitlab/gitlab-oauth-connection.png) + + + + + + + The Access Token method uses a GitLab access token for authentication, providing a straightforward setup process. + + ## Generate GitLab Access Token + + + + Personal access tokens provide access to your GitLab account and all projects you have access to. + + + + Log in to your GitLab account and navigate to User Settings > Access tokens. Click **Add new token** to create a new personal access token. + + ![GitLab Personal Access Tokens](/images/app-connections/gitlab/gitlab-add-access-token.png) + + + + + For Secret Syncs, your token will require the ability to access the API: + Fill in the token details: + - **Token name**: A descriptive name for the token (e.g., "connection-token") + - **Expiration date**: Set an appropriate expiration date + - **Select scopes**: Choose the **api** scope for full API access + + ![GitLab Personal Token Form](/images/app-connections/gitlab/gitlab-personal-access-token-form.png) + + + + + Personal Access Token connections require manual token rotation when your GitLab access token expires or is regenerated. Monitor your connection status and update the token as needed. + + + + Copy the generated token immediately as it won't be shown again. + + ![GitLab Personal Token Created](/images/app-connections/gitlab/gitlab-copy-token.png) + + + Keep your access token secure and do not share it. Anyone with access to this token can access your GitLab account and projects. + + + + + + + Project access tokens provide access to a specific GitLab project, offering more granular control. + + + + Go to your GitLab project and navigate to Settings > Access Tokens. Click **Add new token** to create a new project access token. + + ![GitLab Project Access Tokens](/images/app-connections/gitlab/gitlab-project-access-token-list.png) + + + + + For Secret Syncs, your token will require the ability to access the API and be at least an **Owner**: + Fill in the token details: + - **Token name**: A descriptive name for the token + - **Expiration date**: Set an appropriate expiration date + - **Select role**: Choose **Owner** or higher role + - **Select scopes**: Choose the **api** scope for API access + + ![GitLab Create Project Token](/images/app-connections/gitlab/gitlab-project-access-token-form.png) + + + + + Project Access Token connections require manual token rotation when your GitLab access token expires or is regenerated. Monitor your connection status and update the token as needed. + + + + Copy the generated token immediately as it won't be shown again. + + ![GitLab Project Token Form](/images/app-connections/gitlab/gitlab-project-access-token-created.png) + + + Keep your access token secure and do not share it. Anyone with access to this token can access your GitLab account and projects. + + + + + + + ## Setup GitLab Access Token Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **GitLab Connection** option from the connection options modal. + ![Select GitLab Connection](/images/app-connections/gitlab/select-gitlab-connection.png) + + + Select the **Access Token** method, paste your GitLab access token in the provided field, and select the appropriate token type. + + ![Configure Access Token](/images/app-connections/gitlab/create-gitlab-access-token-connection.png) + + Click **Connect** to establish the connection. + + + Your **GitLab Connection** is now available for use. + ![GitLab Access Token Connection](/images/app-connections/gitlab/gitlab-access-token-connection.png) + + + + + diff --git a/docs/integrations/app-connections/heroku.mdx b/docs/integrations/app-connections/heroku.mdx index d67411627..fc2d1fbcc 100644 --- a/docs/integrations/app-connections/heroku.mdx +++ b/docs/integrations/app-connections/heroku.mdx @@ -1,6 +1,6 @@ --- -title: "Heroku App Connection" -description: "Learn how to configure a Heroku App Connection for Infisical using OAuth or Auth Token methods." +title: "Heroku Connection" +description: "Learn how to configure a Heroku Connection for Infisical using OAuth or Auth Token methods." --- Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth Token**. Choose the method that best fits your setup and security requirements. @@ -10,7 +10,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To The OAuth method provides secure authentication through Heroku's OAuth flow. - Using the Heroku App Connection with OAuth on a self-hosted instance of Infisical requires configuring an API client in Heroku and registering your instance with it. + Using the Heroku Connection with OAuth on a self-hosted instance of Infisical requires configuring an API client in Heroku and registering your instance with it. **Prerequisites:** - A Heroku account with existing applications @@ -42,7 +42,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To - `CLIENT_ID_HEROKU`: The **Client ID** of your Heroku API client. - `CLIENT_SECRET_HEROKU`: The **Client Secret** of your Heroku API client. - Once added, restart your Infisical instance and use the Heroku App Connection. + Once added, restart your Infisical instance and use the Heroku Connection. @@ -55,7 +55,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To ![App Connections Tab](/images/app-connections/general/add-connection.png) - Select the **Heroku App Connection** option from the connection options modal. + Select the **Heroku Connection** option from the connection options modal. ![Select Heroku Connection](/images/app-connections/heroku/heroku-select-connection.png) @@ -68,7 +68,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To ![Heroku Authorization](/images/integrations/heroku/integrations-heroku-auth.png) - Your **Heroku App Connection** is now available for use. + Your **Heroku Connection** is now available for use. ![Heroku OAuth Connection](/images/app-connections/heroku/heroku-connection.png) @@ -97,7 +97,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To ![App Connections Tab](/images/app-connections/general/add-connection.png) - Select the **Heroku App Connection** option from the connection options modal. + Select the **Heroku Connection** option from the connection options modal. ![Select Heroku Connection](/images/app-connections/heroku/heroku-select-connection.png) @@ -108,7 +108,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To Click **Connect** to establish the connection. - Your **Heroku App Connection** is now available for use. + Your **Heroku Connection** is now available for use. ![Heroku Auth Token Connection](/images/app-connections/heroku/heroku-connection.png) diff --git a/docs/integrations/secret-syncs/gitlab.mdx b/docs/integrations/secret-syncs/gitlab.mdx new file mode 100644 index 000000000..5e6cacffa --- /dev/null +++ b/docs/integrations/secret-syncs/gitlab.mdx @@ -0,0 +1,180 @@ +--- +title: "GitLab Sync" +description: "Learn how to configure a GitLab Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [GitLab Connection](/integrations/app-connections/gitlab) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **GitLab** option. + ![Select GitLab](/images/secret-syncs/gitlab/gitlab-secret-sync-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/gitlab/gitlab-secret-sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/gitlab/gitlab-secret-sync-destination.png) + + - **GitLab Connection**: The GitLab Connection to authenticate with. + - **Scope**: The GitLab scope to sync secrets to. + - **Project**: Sync secrets to a GitLab project. + - **Group**: Sync secrets to a GitLab group. +

+ The remaining fields are determined by the selected **Scope**: + + + - **GitLab Project**: The project to deploy secrets to. + - **GitLab Environment Scope**: The environment scope to deploy secrets to (optional, defaults to "*" for all environments). + - **Mark secrets as Protected**: If enabled, synced secrets will be marked as protected in GitLab. + - **Mark secrets as Masked**: If enabled, synced secrets will be masked in GitLab CI/CD logs. + - **Mark secrets as Hidden**: If enabled, synced secrets will be hidden from the GitLab UI. + + + - **GitLab Group**: The group to deploy secrets to. + - **GitLab Environment Scope**: The environment scope to deploy secrets to (optional, defaults to "*" for all environments). + - **Mark secrets as Protected**: If enabled, synced secrets will be marked as protected in GitLab. + - **Mark secrets as Masked**: If enabled, synced secrets will be masked in GitLab CI/CD logs. + - **Mark secrets as Hidden**: If enabled, synced secrets will be hidden from the GitLab UI. + + + + Be aware that GitLab only allows to mark secrets as hidden for new secrets. If you try to mark an existing secret as hidden, it produces an error. + + + If you enable **Mark secrets as Hidden**, Infisical will not be able to unhide/unmask secrets from the sync destination if you disable the option later. This is because GitLab does not allow to unhide/unmask existing secrets. + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/gitlab/gitlab-secret-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + GitLab does not support importing secrets. + + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your GitLab Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/gitlab/gitlab-secret-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your GitLab Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/gitlab/gitlab-secret-sync-review.png) + + 8. If enabled, your GitLab Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/gitlab/gitlab-secret-sync-created.png) + + + + To create a **GitLab Sync**, make an API request to the [Create GitLab Sync](/api-reference/endpoints/secret-syncs/gitlab/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/gitlab \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-gitlab-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "project", + "projectId": "70998370", + "projectName": "test", + "targetEnvironment": "*", + "shouldProtectSecrets": true, + "shouldMaskSecrets": true, + "shouldHideSecrets": false + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-gitlab-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "gitlab", + "name": "my-gitlab-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "gitlab", + "destinationConfig": { + "scope": "project", + "projectId": "70998370", + "projectName": "test", + "targetEnvironment": "*", + "shouldProtectSecrets": true, + "shouldMaskSecrets": true, + "shouldHideSecrets": false + } + } + } + ``` + + + diff --git a/docs/integrations/secret-syncs/heroku.mdx b/docs/integrations/secret-syncs/heroku.mdx index e62b5df9e..b7aa2850a 100644 --- a/docs/integrations/secret-syncs/heroku.mdx +++ b/docs/integrations/secret-syncs/heroku.mdx @@ -6,7 +6,7 @@ description: "Learn how to configure a Heroku Sync for Infisical." **Prerequisites:** - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) -- Create a [Heroku App Connection](/integrations/app-connections/heroku) +- Create a [Heroku Connection](/integrations/app-connections/heroku) @@ -29,7 +29,7 @@ description: "Learn how to configure a Heroku Sync for Infisical." 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. ![Configure Destination](/images/secret-syncs/heroku/heroku-destination.png) - - **Heroku App Connection**: The Heroku App Connection to authenticate with. + - **Heroku Connection**: The Heroku Connection to authenticate with. - **Heroku App**: The Heroku application to sync secrets to. 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. diff --git a/docs/mint.json b/docs/mint.json new file mode 100644 index 000000000..2dcb233d3 --- /dev/null +++ b/docs/mint.json @@ -0,0 +1,2241 @@ +{ + "name": "Infisical", + "openapi": "https://app.infisical.com/api/docs/json", + "logo": { + "dark": "/logo/dark.svg", + "light": "/logo/light.svg", + "href": "https://infisical.com" + }, + "favicon": "/favicon.png", + "colors": { + "primary": "#26272b", + "light": "#97b31d", + "dark": "#A1B659", + "ultraLight": "#E7F256", + "ultraDark": "#8D9F4C", + "background": { + "light": "#ffffff", + "dark": "#0D1117" + }, + "anchors": { + "from": "#000000", + "to": "#707174" + } + }, + "modeToggle": { + "default": "light", + "isHidden": true + }, + "feedback": { + "suggestEdit": true, + "raiseIssue": true, + "thumbsRating": true + }, + "api": { + "baseUrl": ["https://app.infisical.com", "http://localhost:8080"] + }, + "topbarLinks": [ + { + "name": "Log In", + "url": "https://app.infisical.com/login" + } + ], + "topbarCtaButton": { + "name": "Start for Free", + "url": "https://app.infisical.com/signup" + }, + "tabs": [ + { + "name": "Integrations", + "url": "integrations" + }, + { + "name": "CLI", + "url": "cli" + }, + { + "name": "API Reference", + "url": "api-reference" + }, + { + "name": "SDKs", + "url": "sdks" + }, + { + "name": "Changelog", + "url": "changelog" + } + ], + "navigation": [ + { + "group": "Getting Started", + "pages": [ + "documentation/getting-started/introduction", + { + "group": "Quickstart", + "pages": ["documentation/guides/local-development"] + }, + { + "group": "Guides", + "pages": [ + "documentation/guides/introduction", + "documentation/guides/node", + "documentation/guides/python", + "documentation/guides/nextjs-vercel", + "documentation/guides/microsoft-power-apps", + "documentation/guides/organization-structure" + ] + }, + { + "group": "Setup", + "pages": ["documentation/setup/networking"] + } + ] + }, + { + "group": "Platform", + "pages": [ + "documentation/platform/organization", + "documentation/platform/project", + "documentation/platform/folder", + { + "group": "Secrets", + "pages": [ + "documentation/platform/secret-versioning", + "documentation/platform/pit-recovery", + "documentation/platform/secret-reference", + "documentation/platform/webhooks" + ] + }, + { + "group": "Internal PKI", + "pages": [ + "documentation/platform/pki/overview", + "documentation/platform/pki/private-ca", + "documentation/platform/pki/external-ca", + "documentation/platform/pki/subscribers", + "documentation/platform/pki/certificates", + "documentation/platform/pki/acme-ca", + "documentation/platform/pki/est", + "documentation/platform/pki/alerting", + { + "group": "Integrations", + "pages": [ + "documentation/platform/pki/pki-issuer", + "documentation/platform/pki/integration-guides/gloo-mesh" + ] + } + ] + }, + { + "group": "Infisical SSH", + "pages": [ + "documentation/platform/ssh/overview", + "documentation/platform/ssh/host-groups" + ] + }, + { + "group": "Key Management (KMS)", + "pages": [ + "documentation/platform/kms/overview", + "documentation/platform/kms/hsm-integration", + "documentation/platform/kms/kubernetes-encryption", + "documentation/platform/kms/kmip" + ] + }, + { + "group": "KMS Configuration", + "pages": [ + "documentation/platform/kms-configuration/overview", + "documentation/platform/kms-configuration/aws-kms", + "documentation/platform/kms-configuration/aws-hsm", + "documentation/platform/kms-configuration/gcp-kms" + ] + }, + { + "group": "Identities", + "pages": [ + "documentation/platform/identities/overview", + "documentation/platform/identities/user-identities", + "documentation/platform/identities/machine-identities" + ] + }, + { + "group": "Access Control", + "pages": [ + "documentation/platform/access-controls/overview", + "documentation/platform/access-controls/role-based-access-controls", + { + "group": "Attribute based access controls", + "pages": [ + "documentation/platform/access-controls/abac/overview", + "documentation/platform/access-controls/abac/managing-user-metadata", + "documentation/platform/access-controls/abac/managing-machine-identity-attributes" + ] + }, + "documentation/platform/access-controls/additional-privileges", + "documentation/platform/access-controls/temporary-access", + "documentation/platform/access-controls/assume-privilege", + "documentation/platform/access-controls/access-requests", + "documentation/platform/access-controls/project-access-requests", + "documentation/platform/pr-workflows", + "documentation/platform/groups" + ] + }, + { + "group": "Audit Logs", + "pages": [ + "documentation/platform/audit-logs", + "documentation/platform/audit-log-streams/audit-log-streams", + "documentation/platform/audit-log-streams/audit-log-streams-with-fluentbit" + ] + }, + { + "group": "Secret Rotation", + "pages": [ + "documentation/platform/secret-rotation/overview", + "documentation/platform/secret-rotation/auth0-client-secret", + "documentation/platform/secret-rotation/aws-iam-user-secret", + "documentation/platform/secret-rotation/azure-client-secret", + "documentation/platform/secret-rotation/ldap-password", + "documentation/platform/secret-rotation/mssql-credentials", + "documentation/platform/secret-rotation/mysql-credentials", + "documentation/platform/secret-rotation/oracledb-credentials", + "documentation/platform/secret-rotation/postgres-credentials" + ] + }, + { + "group": "Dynamic Secrets", + "pages": [ + "documentation/platform/dynamic-secrets/overview", + "documentation/platform/dynamic-secrets/aws-elasticache", + "documentation/platform/dynamic-secrets/aws-iam", + "documentation/platform/dynamic-secrets/azure-entra-id", + "documentation/platform/dynamic-secrets/cassandra", + "documentation/platform/dynamic-secrets/elastic-search", + "documentation/platform/dynamic-secrets/gcp-iam", + "documentation/platform/dynamic-secrets/ldap", + "documentation/platform/dynamic-secrets/mongo-atlas", + "documentation/platform/dynamic-secrets/mongo-db", + "documentation/platform/dynamic-secrets/mssql", + "documentation/platform/dynamic-secrets/mysql", + "documentation/platform/dynamic-secrets/oracle", + "documentation/platform/dynamic-secrets/postgresql", + "documentation/platform/dynamic-secrets/rabbit-mq", + "documentation/platform/dynamic-secrets/redis", + "documentation/platform/dynamic-secrets/sap-ase", + "documentation/platform/dynamic-secrets/sap-hana", + "documentation/platform/dynamic-secrets/snowflake", + "documentation/platform/dynamic-secrets/totp", + "documentation/platform/dynamic-secrets/kubernetes", + "documentation/platform/dynamic-secrets/vertica" + ] + }, + { + "group": "Gateway", + "pages": [ + "documentation/platform/gateways/overview", + "documentation/platform/gateways/gateway-security", + "documentation/platform/gateways/networking" + ] + }, + "documentation/platform/project-templates", + { + "group": "Workflow Integrations", + "pages": [ + "documentation/platform/workflow-integrations/slack-integration", + "documentation/platform/workflow-integrations/microsoft-teams-integration" + ] + }, + { + "group": "Admin Consoles", + "pages": [ + "documentation/platform/admin-panel/overview", + "documentation/platform/admin-panel/server-admin", + "documentation/platform/admin-panel/org-admin-console" + ] + }, + "documentation/platform/secret-sharing", + { + "group": "Secret Scanning", + "pages": [ + "documentation/platform/secret-scanning/overview", + "documentation/platform/secret-scanning/github" + ] + } + ] + }, + { + "group": "Authentication Methods", + "pages": [ + { + "group": "User Authentication", + "pages": [ + "documentation/platform/auth-methods/email-password", + { + "group": "SSO", + "pages": [ + "documentation/platform/sso/overview", + "documentation/platform/sso/google", + "documentation/platform/sso/github", + "documentation/platform/sso/gitlab", + "documentation/platform/sso/okta", + "documentation/platform/sso/azure", + "documentation/platform/sso/jumpcloud", + "documentation/platform/sso/keycloak-saml", + "documentation/platform/sso/google-saml", + "documentation/platform/sso/auth0-saml", + { + "group": "OIDC", + "pages": [ + { + "group": "Keycloak OIDC", + "pages": [ + "documentation/platform/sso/keycloak-oidc/overview", + "documentation/platform/sso/keycloak-oidc/group-membership-mapping" + ] + }, + "documentation/platform/sso/auth0-oidc", + { + "group": "General OIDC", + "pages": [ + "documentation/platform/sso/general-oidc/overview", + "documentation/platform/sso/general-oidc/group-membership-mapping" + ] + } + ] + } + ] + }, + { + "group": "LDAP", + "pages": [ + "documentation/platform/ldap/overview", + "documentation/platform/ldap/jumpcloud", + "documentation/platform/ldap/general" + ] + }, + { + "group": "SCIM", + "pages": [ + "documentation/platform/scim/overview", + "documentation/platform/scim/okta", + "documentation/platform/scim/azure", + "documentation/platform/scim/jumpcloud", + "documentation/platform/scim/group-mappings" + ] + } + ] + }, + + { + "group": "Machine Identities", + "pages": [ + "documentation/platform/identities/alicloud-auth", + "documentation/platform/identities/aws-auth", + "documentation/platform/identities/azure-auth", + "documentation/platform/identities/gcp-auth", + "documentation/platform/identities/jwt-auth", + "documentation/platform/identities/kubernetes-auth", + "documentation/platform/identities/oci-auth", + "documentation/platform/identities/token-auth", + "documentation/platform/identities/universal-auth", + { + "group": "OIDC Auth", + "pages": [ + "documentation/platform/identities/oidc-auth/general", + "documentation/platform/identities/oidc-auth/azure", + "documentation/platform/identities/oidc-auth/github", + "documentation/platform/identities/oidc-auth/circleci", + "documentation/platform/identities/oidc-auth/gitlab", + "documentation/platform/identities/oidc-auth/terraform-cloud", + "documentation/platform/identities/oidc-auth/spire" + ] + }, + + { + "group": "LDAP Auth", + "pages": [ + "documentation/platform/identities/ldap-auth/general", + "documentation/platform/identities/ldap-auth/jumpcloud" + ] + } + ] + }, + "documentation/platform/token", + "documentation/platform/mfa", + "documentation/platform/github-org-sync" + ] + }, + { + "group": "Self-host Infisical", + "pages": [ + "self-hosting/overview", + { + "group": "Installation methods", + "pages": [ + "self-hosting/deployment-options/standalone-infisical", + "self-hosting/deployment-options/docker-swarm", + "self-hosting/deployment-options/docker-compose", + "self-hosting/deployment-options/kubernetes-helm" + ] + }, + { + "group": "Linux Package", + "pages": [ + "self-hosting/deployment-options/native/linux-package/installation", + "self-hosting/deployment-options/native/linux-package/commands-configuration", + "self-hosting/deployment-options/linux-upgrade" + ] + }, + "self-hosting/guides/upgrading-infisical", + "self-hosting/configuration/envars", + "self-hosting/configuration/requirements", + { + "group": "Guides", + "pages": [ + "self-hosting/guides/mongo-to-postgres", + "self-hosting/guides/custom-certificates", + "self-hosting/guides/automated-bootstrapping", + "self-hosting/guides/production-hardening" + ] + }, + { + "group": "Reference architectures", + "pages": [ + "self-hosting/reference-architectures/aws-ecs", + "self-hosting/reference-architectures/linux-deployment-ha", + "self-hosting/reference-architectures/on-prem-k8s-ha", + "self-hosting/reference-architectures/google-cloud-run" + ] + }, + "self-hosting/ee", + "self-hosting/faq" + ] + }, + { + "group": "Command line", + "pages": [ + "cli/overview", + "cli/usage", + { + "group": "Core commands", + "pages": [ + "cli/commands/login", + "cli/commands/init", + "cli/commands/run", + "cli/commands/secrets", + "cli/commands/dynamic-secrets", + "cli/commands/ssh", + "cli/commands/gateway", + "cli/commands/bootstrap", + "cli/commands/export", + "cli/commands/token", + "cli/commands/service-token", + "cli/commands/vault", + "cli/commands/user", + "cli/commands/reset", + { + "group": "infisical scan", + "pages": [ + "cli/commands/scan", + "cli/commands/scan-git-changes", + "cli/commands/scan-install" + ] + } + ] + }, + "cli/scanning-overview", + "cli/project-config", + "cli/faq" + ] + }, + { + "group": "Infrastructure Integrations", + "pages": [ + "integrations/platforms/ansible", + "integrations/platforms/apache-airflow", + { + "group": "Container orchestrators", + "pages": [ + { + "group": "Kubernetes", + "pages": [ + "integrations/platforms/kubernetes/overview", + "integrations/platforms/kubernetes/infisical-secret-crd", + "integrations/platforms/kubernetes/infisical-push-secret-crd", + "integrations/platforms/kubernetes/infisical-dynamic-secret-crd" + ] + }, + "integrations/platforms/kubernetes-injector", + "integrations/platforms/kubernetes-csi", + "integrations/platforms/docker-swarm-with-agent", + "integrations/platforms/ecs-with-agent" + ] + }, + { + "group": "Docker", + "pages": [ + "integrations/platforms/docker-intro", + "integrations/platforms/docker", + "integrations/platforms/docker-pass-envs", + "integrations/platforms/docker-compose" + ] + }, + "integrations/platforms/infisical-agent", + "integrations/frameworks/packer", + "integrations/frameworks/pulumi", + "integrations/frameworks/terraform" + ] + }, + { + "group": "App Connections", + "pages": [ + "integrations/app-connections/overview", + { + "group": "Connections", + "pages": [ + "integrations/app-connections/1password", + "integrations/app-connections/auth0", + "integrations/app-connections/aws", + "integrations/app-connections/azure-app-configuration", + "integrations/app-connections/azure-client-secrets", + "integrations/app-connections/azure-devops", + "integrations/app-connections/azure-key-vault", + "integrations/app-connections/camunda", + "integrations/app-connections/databricks", + "integrations/app-connections/flyio", + "integrations/app-connections/gcp", + "integrations/app-connections/github", + "integrations/app-connections/github-radar", + "integrations/app-connections/gitlab", + "integrations/app-connections/hashicorp-vault", + "integrations/app-connections/heroku", + "integrations/app-connections/humanitec", + "integrations/app-connections/ldap", + "integrations/app-connections/mssql", + "integrations/app-connections/mysql", + "integrations/app-connections/oci", + "integrations/app-connections/oracledb", + "integrations/app-connections/postgres", + "integrations/app-connections/render", + "integrations/app-connections/teamcity", + "integrations/app-connections/terraform-cloud", + "integrations/app-connections/vercel", + "integrations/app-connections/windmill" + ] + } + ] + }, + { + "group": "Secret Syncs", + "pages": [ + "integrations/secret-syncs/overview", + { + "group": "Syncs", + "pages": [ + "integrations/secret-syncs/1password", + "integrations/secret-syncs/aws-parameter-store", + "integrations/secret-syncs/aws-secrets-manager", + "integrations/secret-syncs/azure-app-configuration", + "integrations/secret-syncs/azure-devops", + "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/camunda", + "integrations/secret-syncs/databricks", + "integrations/secret-syncs/flyio", + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/github", + "integrations/secret-syncs/gitlab", + "integrations/secret-syncs/hashicorp-vault", + "integrations/secret-syncs/heroku", + "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/oci-vault", + "integrations/secret-syncs/render", + "integrations/secret-syncs/teamcity", + "integrations/secret-syncs/terraform-cloud", + "integrations/secret-syncs/vercel", + "integrations/secret-syncs/windmill" + ] + } + ] + }, + { + "group": "Native Integrations", + "pages": [ + { + "group": "AWS", + "pages": [ + "integrations/cloud/aws-parameter-store", + "integrations/cloud/aws-secret-manager", + "integrations/cloud/aws-amplify" + ] + }, + "integrations/cloud/vercel", + "integrations/cloud/azure-key-vault", + "integrations/cloud/azure-app-configuration", + "integrations/cloud/azure-devops", + "integrations/cloud/gcp-secret-manager", + { + "group": "Cloudflare", + "pages": [ + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers" + ] + }, + "integrations/cloud/terraform-cloud", + "integrations/cloud/databricks", + { + "group": "View more", + "pages": [ + "integrations/cloud/digital-ocean-app-platform", + "integrations/cloud/heroku", + "integrations/cloud/netlify", + "integrations/cloud/railway", + "integrations/cloud/flyio", + "integrations/cloud/render", + "integrations/cloud/laravel-forge", + "integrations/cloud/supabase", + "integrations/cloud/northflank", + "integrations/cloud/hasura-cloud", + "integrations/cloud/qovery", + "integrations/cloud/hashicorp-vault", + "integrations/cloud/cloud-66", + "integrations/cloud/windmill" + ] + } + ] + }, + { + "group": "CI/CD Integrations", + "pages": [ + "integrations/cicd/jenkins", + "integrations/cicd/githubactions", + "integrations/cicd/gitlab", + "integrations/cicd/bitbucket", + "integrations/cloud/teamcity", + { + "group": "View more", + "pages": [ + "integrations/cicd/circleci", + "integrations/cicd/travisci", + "integrations/cicd/rundeck", + "integrations/cicd/codefresh", + "integrations/cloud/checkly", + "integrations/cicd/octopus-deploy" + ] + } + ] + }, + { + "group": "Framework Integrations", + "pages": [ + "integrations/frameworks/spring-boot-maven", + "integrations/frameworks/react", + "integrations/frameworks/vue", + "integrations/frameworks/express", + { + "group": "View more", + "pages": [ + "integrations/frameworks/nextjs", + "integrations/frameworks/nestjs", + "integrations/frameworks/sveltekit", + "integrations/frameworks/nuxt", + "integrations/frameworks/gatsby", + "integrations/frameworks/remix", + "integrations/frameworks/vite", + "integrations/frameworks/fiber", + "integrations/frameworks/django", + "integrations/frameworks/flask", + "integrations/frameworks/laravel", + "integrations/frameworks/rails", + "integrations/frameworks/dotnet", + "integrations/platforms/pm2", + "integrations/frameworks/ab-initio" + ] + } + ] + }, + { + "group": "Build Tool Integrations", + "pages": ["integrations/build-tools/gradle"] + }, + { + "group": "Others", + "pages": ["integrations/external/backstage"] + }, + { + "group": "", + "pages": ["sdks/overview"] + }, + { + "group": "SDK's", + "pages": [ + "sdks/languages/node", + "sdks/languages/python", + "sdks/languages/java", + "sdks/languages/csharp", + "sdks/languages/go", + "sdks/languages/ruby" + ] + }, + { + "group": "Overview", + "pages": [ + "api-reference/overview/introduction", + "api-reference/overview/authentication", + { + "group": "Examples", + "pages": ["api-reference/overview/examples/integration"] + } + ] + }, + { + "group": "Endpoints", + "pages": [ + { + "group": "Identities", + "pages": [ + "api-reference/endpoints/identities/create", + "api-reference/endpoints/identities/update", + "api-reference/endpoints/identities/delete", + "api-reference/endpoints/identities/get-by-id", + "api-reference/endpoints/identities/list", + "api-reference/endpoints/identities/search" + ] + }, + { + "group": "Token Auth", + "pages": [ + "api-reference/endpoints/token-auth/attach", + "api-reference/endpoints/token-auth/retrieve", + "api-reference/endpoints/token-auth/update", + "api-reference/endpoints/token-auth/revoke", + "api-reference/endpoints/token-auth/get-tokens", + "api-reference/endpoints/token-auth/create-token", + "api-reference/endpoints/token-auth/update-token", + "api-reference/endpoints/token-auth/revoke-token" + ] + }, + { + "group": "Universal Auth", + "pages": [ + "api-reference/endpoints/universal-auth/login", + "api-reference/endpoints/universal-auth/attach", + "api-reference/endpoints/universal-auth/retrieve", + "api-reference/endpoints/universal-auth/update", + "api-reference/endpoints/universal-auth/revoke", + "api-reference/endpoints/universal-auth/create-client-secret", + "api-reference/endpoints/universal-auth/list-client-secrets", + "api-reference/endpoints/universal-auth/revoke-client-secret", + "api-reference/endpoints/universal-auth/get-client-secret-by-id", + "api-reference/endpoints/universal-auth/renew-access-token", + "api-reference/endpoints/universal-auth/revoke-access-token" + ] + }, + { + "group": "GCP Auth", + "pages": [ + "api-reference/endpoints/gcp-auth/login", + "api-reference/endpoints/gcp-auth/attach", + "api-reference/endpoints/gcp-auth/retrieve", + "api-reference/endpoints/gcp-auth/update", + "api-reference/endpoints/gcp-auth/revoke" + ] + }, + { + "group": "Alibaba Cloud Auth", + "pages": [ + "api-reference/endpoints/alicloud-auth/login", + "api-reference/endpoints/alicloud-auth/attach", + "api-reference/endpoints/alicloud-auth/retrieve", + "api-reference/endpoints/alicloud-auth/update", + "api-reference/endpoints/alicloud-auth/revoke" + ] + }, + { + "group": "AWS Auth", + "pages": [ + "api-reference/endpoints/aws-auth/login", + "api-reference/endpoints/aws-auth/attach", + "api-reference/endpoints/aws-auth/retrieve", + "api-reference/endpoints/aws-auth/update", + "api-reference/endpoints/aws-auth/revoke" + ] + }, + { + "group": "OCI Auth", + "pages": [ + "api-reference/endpoints/oci-auth/login", + "api-reference/endpoints/oci-auth/attach", + "api-reference/endpoints/oci-auth/retrieve", + "api-reference/endpoints/oci-auth/update", + "api-reference/endpoints/oci-auth/revoke" + ] + }, + { + "group": "Azure Auth", + "pages": [ + "api-reference/endpoints/azure-auth/login", + "api-reference/endpoints/azure-auth/attach", + "api-reference/endpoints/azure-auth/retrieve", + "api-reference/endpoints/azure-auth/update", + "api-reference/endpoints/azure-auth/revoke" + ] + }, + { + "group": "Kubernetes Auth", + "pages": [ + "api-reference/endpoints/kubernetes-auth/login", + "api-reference/endpoints/kubernetes-auth/attach", + "api-reference/endpoints/kubernetes-auth/retrieve", + "api-reference/endpoints/kubernetes-auth/update", + "api-reference/endpoints/kubernetes-auth/revoke" + ] + }, + { + "group": "OIDC Auth", + "pages": [ + "api-reference/endpoints/oidc-auth/login", + "api-reference/endpoints/oidc-auth/attach", + "api-reference/endpoints/oidc-auth/retrieve", + "api-reference/endpoints/oidc-auth/update", + "api-reference/endpoints/oidc-auth/revoke" + ] + }, + { + "group": "JWT Auth", + "pages": [ + "api-reference/endpoints/jwt-auth/login", + "api-reference/endpoints/jwt-auth/attach", + "api-reference/endpoints/jwt-auth/retrieve", + "api-reference/endpoints/jwt-auth/update", + "api-reference/endpoints/jwt-auth/revoke" + ] + }, + { + "group": "LDAP Auth", + "pages": [ + "api-reference/endpoints/ldap-auth/login", + "api-reference/endpoints/ldap-auth/attach", + "api-reference/endpoints/ldap-auth/retrieve", + "api-reference/endpoints/ldap-auth/update", + "api-reference/endpoints/ldap-auth/revoke" + ] + }, + { + "group": "Groups", + "pages": [ + "api-reference/endpoints/groups/create", + "api-reference/endpoints/groups/update", + "api-reference/endpoints/groups/delete", + "api-reference/endpoints/groups/get", + "api-reference/endpoints/groups/get-by-id", + "api-reference/endpoints/groups/add-group-user", + "api-reference/endpoints/groups/remove-group-user", + "api-reference/endpoints/groups/list-group-users" + ] + }, + { + "group": "Organizations", + "pages": [ + "api-reference/endpoints/organizations/memberships", + "api-reference/endpoints/organizations/update-membership", + "api-reference/endpoints/organizations/delete-membership", + "api-reference/endpoints/organizations/list-identity-memberships", + "api-reference/endpoints/organizations/workspaces" + ] + }, + { + "group": "Projects", + "pages": [ + "api-reference/endpoints/workspaces/create-workspace", + "api-reference/endpoints/workspaces/delete-workspace", + "api-reference/endpoints/workspaces/get-workspace", + "api-reference/endpoints/workspaces/update-workspace", + "api-reference/endpoints/workspaces/secret-snapshots" + ] + }, + { + "group": "Project Users", + "pages": [ + "api-reference/endpoints/project-users/invite-member-to-workspace", + "api-reference/endpoints/project-users/remove-member-from-workspace", + "api-reference/endpoints/project-users/memberships", + "api-reference/endpoints/project-users/get-by-username", + "api-reference/endpoints/project-users/update-membership" + ] + }, + { + "group": "Project Groups", + "pages": [ + "api-reference/endpoints/project-groups/create", + "api-reference/endpoints/project-groups/delete", + "api-reference/endpoints/project-groups/get-by-id", + "api-reference/endpoints/project-groups/list", + "api-reference/endpoints/project-groups/update" + ] + }, + { + "group": "Project Identities", + "pages": [ + "api-reference/endpoints/project-identities/add-identity-membership", + "api-reference/endpoints/project-identities/list-identity-memberships", + "api-reference/endpoints/project-identities/get-by-id", + "api-reference/endpoints/project-identities/update-identity-membership", + "api-reference/endpoints/project-identities/delete-identity-membership" + ] + }, + { + "group": "Project Roles", + "pages": [ + "api-reference/endpoints/project-roles/create", + "api-reference/endpoints/project-roles/update", + "api-reference/endpoints/project-roles/delete", + "api-reference/endpoints/project-roles/get-by-slug", + "api-reference/endpoints/project-roles/list" + ] + }, + { + "group": "Project Templates", + "pages": [ + "api-reference/endpoints/project-templates/create", + "api-reference/endpoints/project-templates/update", + "api-reference/endpoints/project-templates/delete", + "api-reference/endpoints/project-templates/get-by-id", + "api-reference/endpoints/project-templates/list" + ] + }, + { + "group": "Environments", + "pages": [ + "api-reference/endpoints/environments/create", + "api-reference/endpoints/environments/update", + "api-reference/endpoints/environments/delete" + ] + }, + { + "group": "Folders", + "pages": [ + "api-reference/endpoints/folders/list", + "api-reference/endpoints/folders/get-by-id", + "api-reference/endpoints/folders/create", + "api-reference/endpoints/folders/update", + "api-reference/endpoints/folders/delete" + ] + }, + { + "group": "Secret Tags", + "pages": [ + "api-reference/endpoints/secret-tags/list", + "api-reference/endpoints/secret-tags/get-by-id", + "api-reference/endpoints/secret-tags/get-by-slug", + "api-reference/endpoints/secret-tags/create", + "api-reference/endpoints/secret-tags/update", + "api-reference/endpoints/secret-tags/delete" + ] + }, + { + "group": "Secrets", + "pages": [ + "api-reference/endpoints/secrets/list", + "api-reference/endpoints/secrets/create", + "api-reference/endpoints/secrets/read", + "api-reference/endpoints/secrets/update", + "api-reference/endpoints/secrets/delete", + "api-reference/endpoints/secrets/create-many", + "api-reference/endpoints/secrets/update-many", + "api-reference/endpoints/secrets/delete-many", + "api-reference/endpoints/secrets/attach-tags", + "api-reference/endpoints/secrets/detach-tags" + ] + }, + { + "group": "Dynamic Secrets", + "pages": [ + { + "group": "Kubernetes", + "pages": [ + "api-reference/endpoints/dynamic-secrets/kubernetes/create-lease" + ] + }, + "api-reference/endpoints/dynamic-secrets/create", + "api-reference/endpoints/dynamic-secrets/update", + "api-reference/endpoints/dynamic-secrets/delete", + "api-reference/endpoints/dynamic-secrets/get", + "api-reference/endpoints/dynamic-secrets/list", + "api-reference/endpoints/dynamic-secrets/list-leases", + "api-reference/endpoints/dynamic-secrets/create-lease", + "api-reference/endpoints/dynamic-secrets/delete-lease", + "api-reference/endpoints/dynamic-secrets/renew-lease", + "api-reference/endpoints/dynamic-secrets/get-lease" + ] + }, + { + "group": "Secret Imports", + "pages": [ + "api-reference/endpoints/secret-imports/list", + "api-reference/endpoints/secret-imports/create", + "api-reference/endpoints/secret-imports/update", + "api-reference/endpoints/secret-imports/delete" + ] + }, + { + "group": "Secret Rotations", + "pages": [ + "api-reference/endpoints/secret-rotations/list", + "api-reference/endpoints/secret-rotations/options", + { + "group": "Auth0 Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/auth0-client-secret/create", + "api-reference/endpoints/secret-rotations/auth0-client-secret/delete", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/list", + "api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/auth0-client-secret/update" + ] + }, + { + "group": "AWS IAM User Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/create", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/delete", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-id", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-name", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/list", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/update" + ] + }, + { + "group": "Azure Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/azure-client-secret/create", + "api-reference/endpoints/secret-rotations/azure-client-secret/delete", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/azure-client-secret/list", + "api-reference/endpoints/secret-rotations/azure-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/azure-client-secret/update" + ] + }, + { + "group": "LDAP Password", + "pages": [ + "api-reference/endpoints/secret-rotations/ldap-password/create", + "api-reference/endpoints/secret-rotations/ldap-password/delete", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-name", + "api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/list", + "api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets", + "api-reference/endpoints/secret-rotations/ldap-password/update" + ] + }, + { + "group": "Microsoft SQL Server Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mssql-credentials/create", + "api-reference/endpoints/secret-rotations/mssql-credentials/delete", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mssql-credentials/list", + "api-reference/endpoints/secret-rotations/mssql-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mssql-credentials/update" + ] + }, + { + "group": "MySQL Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mysql-credentials/create", + "api-reference/endpoints/secret-rotations/mysql-credentials/delete", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mysql-credentials/list", + "api-reference/endpoints/secret-rotations/mysql-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mysql-credentials/update" + ] + }, + { + "group": "OracleDB Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/oracledb-credentials/create", + "api-reference/endpoints/secret-rotations/oracledb-credentials/delete", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/oracledb-credentials/list", + "api-reference/endpoints/secret-rotations/oracledb-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/oracledb-credentials/update" + ] + }, + { + "group": "PostgreSQL Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/postgres-credentials/create", + "api-reference/endpoints/secret-rotations/postgres-credentials/delete", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/postgres-credentials/list", + "api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/postgres-credentials/update" + ] + } + ] + }, + { + "group": "Secret Scanning", + "pages": [ + { + "group": "Data Sources", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/list", + "api-reference/endpoints/secret-scanning/data-sources/options", + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/github/list", + "api-reference/endpoints/secret-scanning/data-sources/github/get-by-id", + "api-reference/endpoints/secret-scanning/data-sources/github/get-by-name", + "api-reference/endpoints/secret-scanning/data-sources/github/list-resources", + "api-reference/endpoints/secret-scanning/data-sources/github/list-scans", + "api-reference/endpoints/secret-scanning/data-sources/github/create", + "api-reference/endpoints/secret-scanning/data-sources/github/update", + "api-reference/endpoints/secret-scanning/data-sources/github/delete", + "api-reference/endpoints/secret-scanning/data-sources/github/scan", + "api-reference/endpoints/secret-scanning/data-sources/github/scan-resource" + ] + } + ] + }, + { + "group": "Findings", + "pages": [ + "api-reference/endpoints/secret-scanning/findings/list", + "api-reference/endpoints/secret-scanning/findings/update" + ] + }, + { + "group": "Configuration", + "pages": [ + "api-reference/endpoints/secret-scanning/config/get-by-project-id", + "api-reference/endpoints/secret-scanning/config/update" + ] + } + ] + }, + { + "group": "Identity Specific Privilege", + "pages": [ + { + "group": "V1 (Legacy)", + "pages": [ + "api-reference/endpoints/identity-specific-privilege/v1/create-permanent", + "api-reference/endpoints/identity-specific-privilege/v1/create-temporary", + "api-reference/endpoints/identity-specific-privilege/v1/update", + "api-reference/endpoints/identity-specific-privilege/v1/delete", + "api-reference/endpoints/identity-specific-privilege/v1/find-by-slug", + "api-reference/endpoints/identity-specific-privilege/v1/list" + ] + }, + { + "group": "V2", + "pages": [ + "api-reference/endpoints/identity-specific-privilege/v2/create", + "api-reference/endpoints/identity-specific-privilege/v2/update", + "api-reference/endpoints/identity-specific-privilege/v2/delete", + "api-reference/endpoints/identity-specific-privilege/v2/list", + "api-reference/endpoints/identity-specific-privilege/v2/find-by-id", + "api-reference/endpoints/identity-specific-privilege/v2/find-by-slug" + ] + } + ] + }, + { + "group": "App Connections", + "pages": [ + "api-reference/endpoints/app-connections/list", + "api-reference/endpoints/app-connections/options", + { + "group": "1Password", + "pages": [ + "api-reference/endpoints/app-connections/1password/list", + "api-reference/endpoints/app-connections/1password/available", + "api-reference/endpoints/app-connections/1password/get-by-id", + "api-reference/endpoints/app-connections/1password/get-by-name", + "api-reference/endpoints/app-connections/1password/create", + "api-reference/endpoints/app-connections/1password/update", + "api-reference/endpoints/app-connections/1password/delete" + ] + }, + { + "group": "Auth0", + "pages": [ + "api-reference/endpoints/app-connections/auth0/list", + "api-reference/endpoints/app-connections/auth0/available", + "api-reference/endpoints/app-connections/auth0/get-by-id", + "api-reference/endpoints/app-connections/auth0/get-by-name", + "api-reference/endpoints/app-connections/auth0/create", + "api-reference/endpoints/app-connections/auth0/update", + "api-reference/endpoints/app-connections/auth0/delete" + ] + }, + { + "group": "AWS", + "pages": [ + "api-reference/endpoints/app-connections/aws/list", + "api-reference/endpoints/app-connections/aws/available", + "api-reference/endpoints/app-connections/aws/get-by-id", + "api-reference/endpoints/app-connections/aws/get-by-name", + "api-reference/endpoints/app-connections/aws/create", + "api-reference/endpoints/app-connections/aws/update", + "api-reference/endpoints/app-connections/aws/delete" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/app-connections/azure-app-configuration/list", + "api-reference/endpoints/app-connections/azure-app-configuration/available", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-id", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-name", + "api-reference/endpoints/app-connections/azure-app-configuration/create", + "api-reference/endpoints/app-connections/azure-app-configuration/update", + "api-reference/endpoints/app-connections/azure-app-configuration/delete" + ] + }, + { + "group": "Azure Client Secret", + "pages": [ + "api-reference/endpoints/app-connections/azure-client-secret/list", + "api-reference/endpoints/app-connections/azure-client-secret/available", + "api-reference/endpoints/app-connections/azure-client-secret/get-by-id", + "api-reference/endpoints/app-connections/azure-client-secret/get-by-name", + "api-reference/endpoints/app-connections/azure-client-secret/create", + "api-reference/endpoints/app-connections/azure-client-secret/update", + "api-reference/endpoints/app-connections/azure-client-secret/delete" + ] + }, + { + "group": "Azure DevOps", + "pages": [ + "api-reference/endpoints/app-connections/azure-devops/list", + "api-reference/endpoints/app-connections/azure-devops/available", + "api-reference/endpoints/app-connections/azure-devops/get-by-id", + "api-reference/endpoints/app-connections/azure-devops/get-by-name", + "api-reference/endpoints/app-connections/azure-devops/create", + "api-reference/endpoints/app-connections/azure-devops/update", + "api-reference/endpoints/app-connections/azure-devops/delete" + ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/app-connections/azure-key-vault/list", + "api-reference/endpoints/app-connections/azure-key-vault/available", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-id", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-name", + "api-reference/endpoints/app-connections/azure-key-vault/create", + "api-reference/endpoints/app-connections/azure-key-vault/update", + "api-reference/endpoints/app-connections/azure-key-vault/delete" + ] + }, + { + "group": "Camunda", + "pages": [ + "api-reference/endpoints/app-connections/camunda/list", + "api-reference/endpoints/app-connections/camunda/available", + "api-reference/endpoints/app-connections/camunda/get-by-id", + "api-reference/endpoints/app-connections/camunda/get-by-name", + "api-reference/endpoints/app-connections/camunda/create", + "api-reference/endpoints/app-connections/camunda/update", + "api-reference/endpoints/app-connections/camunda/delete" + ] + }, + { + "group": "Databricks", + "pages": [ + "api-reference/endpoints/app-connections/databricks/list", + "api-reference/endpoints/app-connections/databricks/available", + "api-reference/endpoints/app-connections/databricks/get-by-id", + "api-reference/endpoints/app-connections/databricks/get-by-name", + "api-reference/endpoints/app-connections/databricks/create", + "api-reference/endpoints/app-connections/databricks/update", + "api-reference/endpoints/app-connections/databricks/delete" + ] + }, + { + "group": "Fly.io", + "pages": [ + "api-reference/endpoints/app-connections/flyio/list", + "api-reference/endpoints/app-connections/flyio/available", + "api-reference/endpoints/app-connections/flyio/get-by-id", + "api-reference/endpoints/app-connections/flyio/get-by-name", + "api-reference/endpoints/app-connections/flyio/create", + "api-reference/endpoints/app-connections/flyio/update", + "api-reference/endpoints/app-connections/flyio/delete" + ] + }, + { + "group": "GCP", + "pages": [ + "api-reference/endpoints/app-connections/gcp/list", + "api-reference/endpoints/app-connections/gcp/available", + "api-reference/endpoints/app-connections/gcp/get-by-id", + "api-reference/endpoints/app-connections/gcp/get-by-name", + "api-reference/endpoints/app-connections/gcp/create", + "api-reference/endpoints/app-connections/gcp/update", + "api-reference/endpoints/app-connections/gcp/delete" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/app-connections/github/list", + "api-reference/endpoints/app-connections/github/available", + "api-reference/endpoints/app-connections/github/get-by-id", + "api-reference/endpoints/app-connections/github/get-by-name", + "api-reference/endpoints/app-connections/github/create", + "api-reference/endpoints/app-connections/github/update", + "api-reference/endpoints/app-connections/github/delete" + ] + }, + { + "group": "GitLab", + "pages": [ + "api-reference/endpoints/app-connections/gitlab/list", + "api-reference/endpoints/app-connections/gitlab/available", + "api-reference/endpoints/app-connections/gitlab/get-by-id", + "api-reference/endpoints/app-connections/gitlab/get-by-name", + "api-reference/endpoints/app-connections/gitlab/create", + "api-reference/endpoints/app-connections/gitlab/update", + "api-reference/endpoints/app-connections/gitlab/delete" + ] + }, + { + "group": "GitHub Radar", + "pages": [ + "api-reference/endpoints/app-connections/github-radar/list", + "api-reference/endpoints/app-connections/github-radar/available", + "api-reference/endpoints/app-connections/github-radar/get-by-id", + "api-reference/endpoints/app-connections/github-radar/get-by-name", + "api-reference/endpoints/app-connections/github-radar/create", + "api-reference/endpoints/app-connections/github-radar/update", + "api-reference/endpoints/app-connections/github-radar/delete" + ] + }, + { + "group": "Hashicorp Vault", + "pages": [ + "api-reference/endpoints/app-connections/hashicorp-vault/list", + "api-reference/endpoints/app-connections/hashicorp-vault/available", + "api-reference/endpoints/app-connections/hashicorp-vault/get-by-id", + "api-reference/endpoints/app-connections/hashicorp-vault/get-by-name", + "api-reference/endpoints/app-connections/hashicorp-vault/create", + "api-reference/endpoints/app-connections/hashicorp-vault/update", + "api-reference/endpoints/app-connections/hashicorp-vault/delete" + ] + }, + { + "group": "Heroku", + "pages": [ + "api-reference/endpoints/app-connections/heroku/list", + "api-reference/endpoints/app-connections/heroku/available", + "api-reference/endpoints/app-connections/heroku/get-by-id", + "api-reference/endpoints/app-connections/heroku/get-by-name", + "api-reference/endpoints/app-connections/heroku/create", + "api-reference/endpoints/app-connections/heroku/update", + "api-reference/endpoints/app-connections/heroku/delete" + ] + }, + { + "group": "Humanitec", + "pages": [ + "api-reference/endpoints/app-connections/humanitec/list", + "api-reference/endpoints/app-connections/humanitec/available", + "api-reference/endpoints/app-connections/humanitec/get-by-id", + "api-reference/endpoints/app-connections/humanitec/get-by-name", + "api-reference/endpoints/app-connections/humanitec/create", + "api-reference/endpoints/app-connections/humanitec/update", + "api-reference/endpoints/app-connections/humanitec/delete" + ] + }, + { + "group": "LDAP", + "pages": [ + "api-reference/endpoints/app-connections/ldap/list", + "api-reference/endpoints/app-connections/ldap/available", + "api-reference/endpoints/app-connections/ldap/get-by-id", + "api-reference/endpoints/app-connections/ldap/get-by-name", + "api-reference/endpoints/app-connections/ldap/create", + "api-reference/endpoints/app-connections/ldap/update", + "api-reference/endpoints/app-connections/ldap/delete" + ] + }, + { + "group": "Microsoft SQL Server", + "pages": [ + "api-reference/endpoints/app-connections/mssql/list", + "api-reference/endpoints/app-connections/mssql/available", + "api-reference/endpoints/app-connections/mssql/get-by-id", + "api-reference/endpoints/app-connections/mssql/get-by-name", + "api-reference/endpoints/app-connections/mssql/create", + "api-reference/endpoints/app-connections/mssql/update", + "api-reference/endpoints/app-connections/mssql/delete" + ] + }, + { + "group": "MySQL", + "pages": [ + "api-reference/endpoints/app-connections/mysql/list", + "api-reference/endpoints/app-connections/mysql/available", + "api-reference/endpoints/app-connections/mysql/get-by-id", + "api-reference/endpoints/app-connections/mysql/get-by-name", + "api-reference/endpoints/app-connections/mysql/create", + "api-reference/endpoints/app-connections/mysql/update", + "api-reference/endpoints/app-connections/mysql/delete" + ] + }, + { + "group": "OCI", + "pages": [ + "api-reference/endpoints/app-connections/oci/list", + "api-reference/endpoints/app-connections/oci/available", + "api-reference/endpoints/app-connections/oci/get-by-id", + "api-reference/endpoints/app-connections/oci/get-by-name", + "api-reference/endpoints/app-connections/oci/create", + "api-reference/endpoints/app-connections/oci/update", + "api-reference/endpoints/app-connections/oci/delete" + ] + }, + { + "group": "OracleDB", + "pages": [ + "api-reference/endpoints/app-connections/oracledb/list", + "api-reference/endpoints/app-connections/oracledb/available", + "api-reference/endpoints/app-connections/oracledb/get-by-id", + "api-reference/endpoints/app-connections/oracledb/get-by-name", + "api-reference/endpoints/app-connections/oracledb/create", + "api-reference/endpoints/app-connections/oracledb/update", + "api-reference/endpoints/app-connections/oracledb/delete" + ] + }, + { + "group": "PostgreSQL", + "pages": [ + "api-reference/endpoints/app-connections/postgres/list", + "api-reference/endpoints/app-connections/postgres/available", + "api-reference/endpoints/app-connections/postgres/get-by-id", + "api-reference/endpoints/app-connections/postgres/get-by-name", + "api-reference/endpoints/app-connections/postgres/create", + "api-reference/endpoints/app-connections/postgres/update", + "api-reference/endpoints/app-connections/postgres/delete" + ] + }, + { + "group": "Render", + "pages": [ + "api-reference/endpoints/app-connections/render/list", + "api-reference/endpoints/app-connections/render/available", + "api-reference/endpoints/app-connections/render/get-by-id", + "api-reference/endpoints/app-connections/render/get-by-name", + "api-reference/endpoints/app-connections/render/create", + "api-reference/endpoints/app-connections/render/update", + "api-reference/endpoints/app-connections/render/delete" + ] + }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/app-connections/teamcity/list", + "api-reference/endpoints/app-connections/teamcity/available", + "api-reference/endpoints/app-connections/teamcity/get-by-id", + "api-reference/endpoints/app-connections/teamcity/get-by-name", + "api-reference/endpoints/app-connections/teamcity/create", + "api-reference/endpoints/app-connections/teamcity/update", + "api-reference/endpoints/app-connections/teamcity/delete" + ] + }, + { + "group": "Terraform Cloud", + "pages": [ + "api-reference/endpoints/app-connections/terraform-cloud/list", + "api-reference/endpoints/app-connections/terraform-cloud/available", + "api-reference/endpoints/app-connections/terraform-cloud/get-by-id", + "api-reference/endpoints/app-connections/terraform-cloud/get-by-name", + "api-reference/endpoints/app-connections/terraform-cloud/create", + "api-reference/endpoints/app-connections/terraform-cloud/update", + "api-reference/endpoints/app-connections/terraform-cloud/delete" + ] + }, + { + "group": "Vercel", + "pages": [ + "api-reference/endpoints/app-connections/vercel/list", + "api-reference/endpoints/app-connections/vercel/available", + "api-reference/endpoints/app-connections/vercel/get-by-id", + "api-reference/endpoints/app-connections/vercel/get-by-name", + "api-reference/endpoints/app-connections/vercel/create", + "api-reference/endpoints/app-connections/vercel/update", + "api-reference/endpoints/app-connections/vercel/delete" + ] + }, + { + "group": "Windmill", + "pages": [ + "api-reference/endpoints/app-connections/windmill/list", + "api-reference/endpoints/app-connections/windmill/available", + "api-reference/endpoints/app-connections/windmill/get-by-id", + "api-reference/endpoints/app-connections/windmill/get-by-name", + "api-reference/endpoints/app-connections/windmill/create", + "api-reference/endpoints/app-connections/windmill/update", + "api-reference/endpoints/app-connections/windmill/delete" + ] + } + ] + }, + { + "group": "Secret Syncs", + "pages": [ + "api-reference/endpoints/secret-syncs/list", + "api-reference/endpoints/secret-syncs/options", + { + "group": "1Password", + "pages": [ + "api-reference/endpoints/secret-syncs/1password/list", + "api-reference/endpoints/secret-syncs/1password/get-by-id", + "api-reference/endpoints/secret-syncs/1password/get-by-name", + "api-reference/endpoints/secret-syncs/1password/create", + "api-reference/endpoints/secret-syncs/1password/update", + "api-reference/endpoints/secret-syncs/1password/delete", + "api-reference/endpoints/secret-syncs/1password/sync-secrets", + "api-reference/endpoints/secret-syncs/1password/import-secrets", + "api-reference/endpoints/secret-syncs/1password/remove-secrets" + ] + }, + { + "group": "AWS Parameter Store", + "pages": [ + "api-reference/endpoints/secret-syncs/aws-parameter-store/list", + "api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id", + "api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-name", + "api-reference/endpoints/secret-syncs/aws-parameter-store/create", + "api-reference/endpoints/secret-syncs/aws-parameter-store/update", + "api-reference/endpoints/secret-syncs/aws-parameter-store/delete", + "api-reference/endpoints/secret-syncs/aws-parameter-store/sync-secrets", + "api-reference/endpoints/secret-syncs/aws-parameter-store/import-secrets", + "api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets" + ] + }, + { + "group": "AWS Secrets Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/aws-secrets-manager/list", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/create", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/update", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/delete", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-app-configuration/list", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name", + "api-reference/endpoints/secret-syncs/azure-app-configuration/create", + "api-reference/endpoints/secret-syncs/azure-app-configuration/update", + "api-reference/endpoints/secret-syncs/azure-app-configuration/delete", + "api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets" + ] + }, + { + "group": "Azure DevOps", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-devops/list", + "api-reference/endpoints/secret-syncs/azure-devops/get-by-id", + "api-reference/endpoints/secret-syncs/azure-devops/get-by-name", + "api-reference/endpoints/secret-syncs/azure-devops/create", + "api-reference/endpoints/secret-syncs/azure-devops/update", + "api-reference/endpoints/secret-syncs/azure-devops/delete", + "api-reference/endpoints/secret-syncs/azure-devops/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-devops/import-secrets", + "api-reference/endpoints/secret-syncs/azure-devops/remove-secrets" + ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-key-vault/list", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name", + "api-reference/endpoints/secret-syncs/azure-key-vault/create", + "api-reference/endpoints/secret-syncs/azure-key-vault/update", + "api-reference/endpoints/secret-syncs/azure-key-vault/delete", + "api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets" + ] + }, + { + "group": "Camunda", + "pages": [ + "api-reference/endpoints/secret-syncs/camunda/list", + "api-reference/endpoints/secret-syncs/camunda/get-by-id", + "api-reference/endpoints/secret-syncs/camunda/get-by-name", + "api-reference/endpoints/secret-syncs/camunda/create", + "api-reference/endpoints/secret-syncs/camunda/update", + "api-reference/endpoints/secret-syncs/camunda/delete", + "api-reference/endpoints/secret-syncs/camunda/sync-secrets", + "api-reference/endpoints/secret-syncs/camunda/remove-secrets" + ] + }, + { + "group": "Databricks", + "pages": [ + "api-reference/endpoints/secret-syncs/databricks/list", + "api-reference/endpoints/secret-syncs/databricks/get-by-id", + "api-reference/endpoints/secret-syncs/databricks/get-by-name", + "api-reference/endpoints/secret-syncs/databricks/create", + "api-reference/endpoints/secret-syncs/databricks/update", + "api-reference/endpoints/secret-syncs/databricks/delete", + "api-reference/endpoints/secret-syncs/databricks/sync-secrets", + "api-reference/endpoints/secret-syncs/databricks/remove-secrets" + ] + }, + { + "group": "Fly.io", + "pages": [ + "api-reference/endpoints/secret-syncs/flyio/list", + "api-reference/endpoints/secret-syncs/flyio/get-by-id", + "api-reference/endpoints/secret-syncs/flyio/get-by-name", + "api-reference/endpoints/secret-syncs/flyio/create", + "api-reference/endpoints/secret-syncs/flyio/update", + "api-reference/endpoints/secret-syncs/flyio/delete", + "api-reference/endpoints/secret-syncs/flyio/sync-secrets", + "api-reference/endpoints/secret-syncs/flyio/remove-secrets" + ] + }, + { + "group": "GCP Secret Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/gcp-secret-manager/list", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/create", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/update", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/delete", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/secret-syncs/github/list", + "api-reference/endpoints/secret-syncs/github/get-by-id", + "api-reference/endpoints/secret-syncs/github/get-by-name", + "api-reference/endpoints/secret-syncs/github/create", + "api-reference/endpoints/secret-syncs/github/update", + "api-reference/endpoints/secret-syncs/github/delete", + "api-reference/endpoints/secret-syncs/github/sync-secrets", + "api-reference/endpoints/secret-syncs/github/remove-secrets" + ] + }, + { + "group": "GitLab", + "pages": [ + "api-reference/endpoints/secret-syncs/gitlab/list", + "api-reference/endpoints/secret-syncs/gitlab/get-by-id", + "api-reference/endpoints/secret-syncs/gitlab/get-by-name", + "api-reference/endpoints/secret-syncs/gitlab/create", + "api-reference/endpoints/secret-syncs/gitlab/update", + "api-reference/endpoints/secret-syncs/gitlab/delete", + "api-reference/endpoints/secret-syncs/gitlab/sync-secrets", + "api-reference/endpoints/secret-syncs/gitlab/remove-secrets" + ] + }, + { + "group": "Hashicorp Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/hashicorp-vault/list", + "api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id", + "api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name", + "api-reference/endpoints/secret-syncs/hashicorp-vault/create", + "api-reference/endpoints/secret-syncs/hashicorp-vault/update", + "api-reference/endpoints/secret-syncs/hashicorp-vault/delete", + "api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets", + "api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets" + ] + }, + { + "group": "Heroku", + "pages": [ + "api-reference/endpoints/secret-syncs/heroku/list", + "api-reference/endpoints/secret-syncs/heroku/get-by-id", + "api-reference/endpoints/secret-syncs/heroku/get-by-name", + "api-reference/endpoints/secret-syncs/heroku/create", + "api-reference/endpoints/secret-syncs/heroku/update", + "api-reference/endpoints/secret-syncs/heroku/delete", + "api-reference/endpoints/secret-syncs/heroku/sync-secrets", + "api-reference/endpoints/secret-syncs/heroku/remove-secrets" + ] + }, + { + "group": "Humanitec", + "pages": [ + "api-reference/endpoints/secret-syncs/humanitec/list", + "api-reference/endpoints/secret-syncs/humanitec/get-by-id", + "api-reference/endpoints/secret-syncs/humanitec/get-by-name", + "api-reference/endpoints/secret-syncs/humanitec/create", + "api-reference/endpoints/secret-syncs/humanitec/update", + "api-reference/endpoints/secret-syncs/humanitec/delete", + "api-reference/endpoints/secret-syncs/humanitec/sync-secrets", + "api-reference/endpoints/secret-syncs/humanitec/remove-secrets" + ] + }, + { + "group": "OCI", + "pages": [ + "api-reference/endpoints/secret-syncs/oci-vault/list", + "api-reference/endpoints/secret-syncs/oci-vault/get-by-id", + "api-reference/endpoints/secret-syncs/oci-vault/get-by-name", + "api-reference/endpoints/secret-syncs/oci-vault/create", + "api-reference/endpoints/secret-syncs/oci-vault/update", + "api-reference/endpoints/secret-syncs/oci-vault/delete", + "api-reference/endpoints/secret-syncs/oci-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/oci-vault/import-secrets", + "api-reference/endpoints/secret-syncs/oci-vault/remove-secrets" + ] + }, + { + "group": "Render", + "pages": [ + "api-reference/endpoints/secret-syncs/render/list", + "api-reference/endpoints/secret-syncs/render/get-by-id", + "api-reference/endpoints/secret-syncs/render/get-by-name", + "api-reference/endpoints/secret-syncs/render/create", + "api-reference/endpoints/secret-syncs/render/update", + "api-reference/endpoints/secret-syncs/render/delete", + "api-reference/endpoints/secret-syncs/render/sync-secrets", + "api-reference/endpoints/secret-syncs/render/import-secrets", + "api-reference/endpoints/secret-syncs/render/remove-secrets" + ] + }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/secret-syncs/teamcity/list", + "api-reference/endpoints/secret-syncs/teamcity/get-by-id", + "api-reference/endpoints/secret-syncs/teamcity/get-by-name", + "api-reference/endpoints/secret-syncs/teamcity/create", + "api-reference/endpoints/secret-syncs/teamcity/update", + "api-reference/endpoints/secret-syncs/teamcity/delete", + "api-reference/endpoints/secret-syncs/teamcity/sync-secrets", + "api-reference/endpoints/secret-syncs/teamcity/import-secrets", + "api-reference/endpoints/secret-syncs/teamcity/remove-secrets" + ] + }, + { + "group": "Terraform Cloud", + "pages": [ + "api-reference/endpoints/secret-syncs/terraform-cloud/list", + "api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id", + "api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name", + "api-reference/endpoints/secret-syncs/terraform-cloud/create", + "api-reference/endpoints/secret-syncs/terraform-cloud/update", + "api-reference/endpoints/secret-syncs/terraform-cloud/delete", + "api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets", + "api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets" + ] + }, + { + "group": "Vercel", + "pages": [ + "api-reference/endpoints/secret-syncs/vercel/list", + "api-reference/endpoints/secret-syncs/vercel/get-by-id", + "api-reference/endpoints/secret-syncs/vercel/get-by-name", + "api-reference/endpoints/secret-syncs/vercel/create", + "api-reference/endpoints/secret-syncs/vercel/update", + "api-reference/endpoints/secret-syncs/vercel/delete", + "api-reference/endpoints/secret-syncs/vercel/sync-secrets", + "api-reference/endpoints/secret-syncs/vercel/import-secrets", + "api-reference/endpoints/secret-syncs/vercel/remove-secrets" + ] + }, + { + "group": "Windmill", + "pages": [ + "api-reference/endpoints/secret-syncs/windmill/list", + "api-reference/endpoints/secret-syncs/windmill/get-by-id", + "api-reference/endpoints/secret-syncs/windmill/get-by-name", + "api-reference/endpoints/secret-syncs/windmill/create", + "api-reference/endpoints/secret-syncs/windmill/update", + "api-reference/endpoints/secret-syncs/windmill/delete", + "api-reference/endpoints/secret-syncs/windmill/sync-secrets", + "api-reference/endpoints/secret-syncs/windmill/import-secrets", + "api-reference/endpoints/secret-syncs/windmill/remove-secrets" + ] + } + ] + }, + { + "group": "Integrations", + "pages": [ + "api-reference/endpoints/integrations/create-auth", + "api-reference/endpoints/integrations/list-auth", + "api-reference/endpoints/integrations/find-auth", + "api-reference/endpoints/integrations/delete-auth", + "api-reference/endpoints/integrations/delete-auth-by-id", + "api-reference/endpoints/integrations/create", + "api-reference/endpoints/integrations/update", + "api-reference/endpoints/integrations/delete", + "api-reference/endpoints/integrations/list-project-integrations" + ] + }, + { + "group": "Service Tokens", + "pages": ["api-reference/endpoints/service-tokens/get"] + }, + { + "group": "Audit Logs", + "pages": ["api-reference/endpoints/audit-logs/export-audit-log"] + } + ] + }, + { + "group": "Infisical PKI", + "pages": [ + { + "group": "Subscribers", + "pages": [ + "api-reference/endpoints/pki/subscribers/list-certs", + "api-reference/endpoints/pki/subscribers/create", + "api-reference/endpoints/pki/subscribers/read", + "api-reference/endpoints/pki/subscribers/update", + "api-reference/endpoints/pki/subscribers/delete", + "api-reference/endpoints/pki/subscribers/issue-cert", + "api-reference/endpoints/pki/subscribers/sign-cert", + "api-reference/endpoints/pki/subscribers/order-cert", + "api-reference/endpoints/pki/subscribers/get-latest-cert-bundle" + ] + }, + { + "group": "Certificate Authorities", + "pages": [ + { + "group": "ACME", + "pages": [ + "api-reference/endpoints/certificate-authorities/acme/list", + "api-reference/endpoints/certificate-authorities/acme/create", + "api-reference/endpoints/certificate-authorities/acme/read", + "api-reference/endpoints/certificate-authorities/acme/update", + "api-reference/endpoints/certificate-authorities/acme/delete" + ] + }, + { + "group": "Internal", + "pages": [ + "api-reference/endpoints/certificate-authorities/internal/list", + "api-reference/endpoints/certificate-authorities/internal/create", + "api-reference/endpoints/certificate-authorities/internal/read", + "api-reference/endpoints/certificate-authorities/internal/update", + "api-reference/endpoints/certificate-authorities/internal/delete" + ] + }, + "api-reference/endpoints/certificate-authorities/list", + "api-reference/endpoints/certificate-authorities/create", + "api-reference/endpoints/certificate-authorities/read", + "api-reference/endpoints/certificate-authorities/update", + "api-reference/endpoints/certificate-authorities/delete", + "api-reference/endpoints/certificate-authorities/renew", + "api-reference/endpoints/certificate-authorities/list-ca-certs", + "api-reference/endpoints/certificate-authorities/csr", + "api-reference/endpoints/certificate-authorities/cert", + "api-reference/endpoints/certificate-authorities/sign-intermediate", + "api-reference/endpoints/certificate-authorities/import-cert", + "api-reference/endpoints/certificate-authorities/issue-cert", + "api-reference/endpoints/certificate-authorities/sign-cert", + "api-reference/endpoints/certificate-authorities/crl" + ] + }, + { + "group": "Certificates", + "pages": [ + "api-reference/endpoints/certificates/list", + "api-reference/endpoints/certificates/read", + "api-reference/endpoints/certificates/revoke", + "api-reference/endpoints/certificates/delete", + "api-reference/endpoints/certificates/cert-body", + "api-reference/endpoints/certificates/bundle", + "api-reference/endpoints/certificates/private-key", + "api-reference/endpoints/certificates/issue-certificate", + "api-reference/endpoints/certificates/sign-certificate" + ] + }, + { + "group": "Certificate Templates", + "pages": [ + "api-reference/endpoints/certificate-templates/create", + "api-reference/endpoints/certificate-templates/update", + "api-reference/endpoints/certificate-templates/get-by-id", + "api-reference/endpoints/certificate-templates/delete" + ] + }, + { + "group": "Certificate Collections", + "pages": [ + "api-reference/endpoints/pki-collections/create", + "api-reference/endpoints/pki-collections/read", + "api-reference/endpoints/pki-collections/update", + "api-reference/endpoints/pki-collections/delete", + "api-reference/endpoints/pki-collections/add-item", + "api-reference/endpoints/pki-collections/list-items", + "api-reference/endpoints/pki-collections/delete-item" + ] + }, + { + "group": "PKI Alerting", + "pages": [ + "api-reference/endpoints/pki-alerts/create", + "api-reference/endpoints/pki-alerts/read", + "api-reference/endpoints/pki-alerts/update", + "api-reference/endpoints/pki-alerts/delete" + ] + } + ] + }, + { + "group": "Infisical SSH", + "pages": [ + { + "group": "Hosts", + "pages": [ + "api-reference/endpoints/ssh/hosts/list-my", + "api-reference/endpoints/ssh/hosts/list", + "api-reference/endpoints/ssh/hosts/create", + "api-reference/endpoints/ssh/hosts/read", + "api-reference/endpoints/ssh/hosts/update", + "api-reference/endpoints/ssh/hosts/delete", + "api-reference/endpoints/ssh/hosts/issue-host-cert", + "api-reference/endpoints/ssh/hosts/issue-user-cert", + "api-reference/endpoints/ssh/hosts/read-user-ca-pk", + "api-reference/endpoints/ssh/hosts/read-host-ca-pk" + ] + }, + { + "group": "Host Groups", + "pages": [ + "api-reference/endpoints/ssh/groups/list", + "api-reference/endpoints/ssh/groups/create", + "api-reference/endpoints/ssh/groups/read", + "api-reference/endpoints/ssh/groups/update", + "api-reference/endpoints/ssh/groups/delete", + "api-reference/endpoints/ssh/groups/add-host", + "api-reference/endpoints/ssh/groups/list-hosts", + "api-reference/endpoints/ssh/groups/remove-host" + ] + }, + { + "group": "Certificates", + "pages": [ + "api-reference/endpoints/ssh/certificates/issue-credentials", + "api-reference/endpoints/ssh/certificates/sign-key" + ] + }, + { + "group": "Certificate Authorities", + "pages": [ + "api-reference/endpoints/ssh/ca/list", + "api-reference/endpoints/ssh/ca/create", + "api-reference/endpoints/ssh/ca/read", + "api-reference/endpoints/ssh/ca/update", + "api-reference/endpoints/ssh/ca/delete", + "api-reference/endpoints/ssh/ca/public-key", + "api-reference/endpoints/ssh/ca/list-certificate-templates" + ] + }, + { + "group": "Certificate Templates", + "pages": [ + "api-reference/endpoints/ssh/certificate-templates/list", + "api-reference/endpoints/ssh/certificate-templates/create", + "api-reference/endpoints/ssh/certificate-templates/read", + "api-reference/endpoints/ssh/certificate-templates/update", + "api-reference/endpoints/ssh/certificate-templates/delete" + ] + } + ] + }, + { + "group": "Infisical KMS", + "pages": [ + { + "group": "Keys", + "pages": [ + "api-reference/endpoints/kms/keys/list", + "api-reference/endpoints/kms/keys/get-by-id", + "api-reference/endpoints/kms/keys/get-by-name", + "api-reference/endpoints/kms/keys/create", + "api-reference/endpoints/kms/keys/update", + "api-reference/endpoints/kms/keys/delete" + ] + }, + { + "group": "Encryption", + "pages": [ + "api-reference/endpoints/kms/encryption/encrypt", + "api-reference/endpoints/kms/encryption/decrypt" + ] + }, + { + "group": "Signing", + "pages": [ + "api-reference/endpoints/kms/signing/sign", + "api-reference/endpoints/kms/signing/verify", + "api-reference/endpoints/kms/signing/public-key", + "api-reference/endpoints/kms/signing/signing-algorithms" + ] + } + ] + }, + { + "group": "Internals", + "pages": [ + "internals/overview", + { + "group": "Permissions", + "pages": [ + "internals/permissions/overview", + "internals/permissions/project-permissions", + "internals/permissions/organization-permissions", + "internals/permissions/migration" + ] + }, + "internals/components", + "internals/security", + "internals/service-tokens" + ] + }, + { + "group": "", + "pages": ["changelog/overview"] + }, + { + "group": "Contributing", + "pages": [ + { + "group": "Getting Started", + "pages": [ + "contributing/getting-started/overview", + "contributing/getting-started/code-of-conduct", + "contributing/getting-started/pull-requests", + "contributing/getting-started/faq" + ] + }, + { + "group": "Contributing to platform", + "pages": [ + "contributing/platform/developing", + "contributing/platform/backend/how-to-create-a-feature", + "contributing/platform/backend/folder-structure" + ] + }, + { + "group": "Contributing to SDK", + "pages": ["contributing/sdk/developing"] + } + ] + } + ], + "analytics": { + "koala": { + "publicApiKey": "pk_b50d7184e0e39ddd5cdb43cf6abeadd9b97d" + } + }, + "footer": { + "socials": { + "x": "https://www.twitter.com/infisical/", + "linkedin": "https://www.linkedin.com/company/infisical/", + "github": "https://github.com/Infisical/infisical-cli", + "slack": "https://infisical.com/slack" + }, + "links": [ + { + "title": "PRODUCT", + "links": [ + { + "label": "Secret Management", + "url": "https://infisical.com/" + }, + { + "label": "Secret Scanning", + "url": "https://infisical.com/radar" + }, + { + "label": "Share Secrets", + "url": "https://app.infisical.com/share-secret" + }, + { + "label": "Pricing", + "url": "https://infisical.com/pricing" + }, + { + "label": "Security", + "url": "https://infisical.com/docs/internals/security" + }, + { + "label": "Blog", + "url": "https://infisical.com/blog" + }, + { + "label": "Infisical vs Vault", + "url": "https://infisical.com/infisical-vs-hashicorp-vault" + }, + { + "label": "Forum", + "url": "https://questions.infisical.com/" + } + ] + }, + { + "title": "USE CASES", + "links": [ + { + "label": "Infisical Agent", + "url": "https://infisical.com/docs/documentation/getting-started/introduction" + }, + { + "label": "Kubernetes", + "url": "https://infisical.com/docs/integrations/platforms/kubernetes" + }, + { + "label": "Dynamic Secrets", + "url": "https://infisical.com/docs/documentation/platform/dynamic-secrets/overview" + }, + { + "label": "Terraform", + "url": "https://infisical.com/docs/integrations/frameworks/terraform" + }, + { + "label": "Ansible", + "url": "https://infisical.com/docs/integrations/platforms/ansible" + }, + { + "label": "Jenkins", + "url": "https://infisical.com/docs/integrations/cicd/jenkins" + }, + { + "label": "Docker", + "url": "https://infisical.com/docs/integrations/platforms/docker-intro" + }, + { + "label": "AWS ECS", + "url": "https://infisical.com/docs/integrations/platforms/ecs-with-agent" + }, + { + "label": "GitLab", + "url": "https://infisical.com/docs/integrations/cicd/gitlab" + }, + { + "label": "GitHub", + "url": "https://infisical.com/docs/integrations/cicd/githubactions" + }, + { + "label": "SDK", + "url": "https://infisical.com/docs/sdks/overview" + } + ] + }, + { + "title": "DEVELOPERS", + "links": [ + { + "label": "Changelog", + "url": "https://www.infisical.com/docs/changelog" + }, + { + "label": "Status", + "url": "https://status.infisical.com/" + }, + { + "label": "Feedback & Requests", + "url": "https://github.com/Infisical/infisical/issues" + }, + { + "label": "Trust of Center", + "url": "https://app.vanta.com/infisical.com/trust/hoop8cr78cuarxo9sztvs" + }, + { + "label": "Open Source Friends", + "url": "https://infisical.com/infisical-friends" + }, + { + "label": "How to contribute", + "url": "https://www.infisical.com/infisical-heroes" + } + ] + }, + { + "title": "OTHERS", + "links": [ + { + "label": "Customers", + "url": "https://infisical.com/customers/traba" + }, + { + "label": "Company Handbook", + "url": "https://infisical.com/wiki/handbook/overview" + }, + { + "label": "Careers", + "url": "https://infisical.com/careers" + }, + { + "label": "Terms of Service", + "url": "https://infisical.com/terms" + }, + { + "label": "Privacy Policy", + "url": "https://infisical.com/privacy" + }, + { + "label": "Subprocessors", + "url": "https://infisical.com/subprocessors" + }, + { + "label": "SLA", + "url": "https://infisical.com/sla" + }, + { + "label": "Team Email", + "url": "mailto:team@infisical.com" + }, + { + "label": "Sales", + "url": "mailto:sales@infisical.com" + }, + { + "label": "Support", + "url": "https://infisical.com/slack" + } + ] + } + ] + } +} diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index efce4d912..f5d1c58d0 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -590,6 +590,17 @@ You can configure third-party app connections for re-use across Infisical Projec + + + The Application ID of your GitLab OAuth application. + + + + The Secret of your GitLab OAuth application. + + + + ## Native Secret Integrations To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box. diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GitLabSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GitLabSyncFields.tsx new file mode 100644 index 000000000..42387e6df --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GitLabSyncFields.tsx @@ -0,0 +1,282 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo, faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { + FilterableSelect, + FormControl, + Input, + Select, + SelectItem, + Switch, + Tooltip +} from "@app/components/v2"; +import { + TGitLabGroup, + TGitLabProject, + useGitlabConnectionListGroups, + useGitlabConnectionListProjects +} from "@app/hooks/api/appConnections/gitlab"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync"; + +import { TSecretSyncForm } from "../schemas"; + +const SecretProtectionOption = ({ + title, + isEnabled, + onChange, + id, + isDisabled = false, + tooltip +}: { + title: string; + isEnabled: boolean; + onChange: (checked: boolean) => void; + id: string; + isDisabled?: boolean; + tooltip?: string; +}) => { + return ( + +

+ {title}{" "} + {tooltip && ( + + + + )} +

+ + ); +}; + +export const GitLabSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.GitLab } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + const scope = useWatch({ name: "destinationConfig.scope", control }); + const shouldMaskSecrets = useWatch({ name: "destinationConfig.shouldMaskSecrets", control }); + + const { data: groups, isLoading: isGroupsLoading } = useGitlabConnectionListGroups(connectionId, { + enabled: Boolean(connectionId) && scope === GitLabSyncScope.Group + }); + + const { data: projects, isLoading: isProjectsLoading } = useGitlabConnectionListProjects( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + return ( +
+ { + setValue("destinationConfig.projectId", ""); + setValue("destinationConfig.projectName", ""); + setValue("destinationConfig.groupId", ""); + setValue("destinationConfig.groupName", ""); + setValue("destinationConfig.scope", GitLabSyncScope.Project); + }} + /> + + ( + + + + )} + /> + + {scope === GitLabSyncScope.Group && ( + ( + +
+ Don't see the group you're looking for?{" "} + +
+ + } + > + group.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? ""); + setValue( + "destinationConfig.groupName", + (option as SingleValue)?.name ?? "" + ); + }} + options={groups} + placeholder="Select a group..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + )} + + {scope === GitLabSyncScope.Project && ( + ( + +
+ Don't see the project you're looking for?{" "} + +
+ + } + > + project.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? ""); + setValue( + "destinationConfig.projectName", + (option as SingleValue)?.name ?? "" + ); + }} + options={projects} + placeholder="Select a project..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + )} + + ( + + + + )} + /> + + {/* Secret Protection Settings Section */} +
+
+ ( + + )} + /> + + ( + { + onChange(checked); + if (!checked) { + setValue("destinationConfig.shouldHideSecrets", false); + } + }} + /> + )} + /> + + ( +
+ +
+ )} + /> +
+
+
+ ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 74824a28e..b6074d270 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -15,6 +15,7 @@ import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { FlyioSyncFields } from "./FlyioSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; +import { GitLabSyncFields } from "./GitLabSyncFields"; import { HCVaultSyncFields } from "./HCVaultSyncFields"; import { HerokuSyncFields } from "./HerokuSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; @@ -71,6 +72,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Flyio: return ; + case SecretSync.GitLab: + return ; case SecretSync.CloudflarePages: return ; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index b44c3ae51..a61c2a6b8 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -55,6 +55,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Heroku: case SecretSync.Render: case SecretSync.Flyio: + case SecretSync.GitLab: case SecretSync.CloudflarePages: AdditionalSyncOptionsFieldsComponent = null; break; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GitLabSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GitLabSyncReviewFields.tsx new file mode 100644 index 000000000..bf2de6c2f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GitLabSyncReviewFields.tsx @@ -0,0 +1,37 @@ +import { useFormContext } from "react-hook-form"; + +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync"; + +export const GitLabSyncReviewFields = () => { + const { watch } = useFormContext(); + const projectName = watch("destinationConfig.projectName"); + const targetEnvironment = watch("destinationConfig.targetEnvironment"); + const groupName = watch("destinationConfig.groupName"); + const scope = watch("destinationConfig.scope"); + const shouldProtectSecrets = watch("destinationConfig.shouldProtectSecrets"); + const shouldMaskSecrets = watch("destinationConfig.shouldMaskSecrets"); + const shouldHideSecrets = watch("destinationConfig.shouldHideSecrets"); + + return ( + <> + {scope} + {scope === GitLabSyncScope.Project && ( + {projectName} + )} + {scope === GitLabSyncScope.Group && ( + {groupName} + )} + {targetEnvironment && ( + {targetEnvironment} + )} + + {shouldProtectSecrets ? "Yes" : "No"} + + {shouldMaskSecrets ? "Yes" : "No"} + {shouldHideSecrets ? "Yes" : "No"} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 26ab0bfcf..fb639e91b 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -24,6 +24,7 @@ import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; +import { GitLabSyncReviewFields } from "./GitLabSyncReviewFields"; import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields"; import { HerokuSyncReviewFields } from "./HerokuSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; @@ -117,6 +118,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Flyio: DestinationFieldsComponent = ; break; + case SecretSync.GitLab: + DestinationFieldsComponent = ; + break; case SecretSync.CloudflarePages: DestinationFieldsComponent = ; break; diff --git a/frontend/src/components/secret-syncs/forms/schemas/gitlab-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/gitlab-sync-destination-schema.ts new file mode 100644 index 000000000..b264d4e2c --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/gitlab-sync-destination-schema.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync"; + +export const GitlabSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.GitLab), + destinationConfig: z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(GitLabSyncScope.Project), + projectId: z.string().trim().min(1, "Project ID required"), + projectName: z.string().trim().min(1, "Project name required"), + targetEnvironment: z.string().optional(), + shouldProtectSecrets: z.boolean().optional().default(false), + shouldMaskSecrets: z.boolean().optional().default(false), + shouldHideSecrets: z.boolean().optional().default(false) + }), + z.object({ + scope: z.literal(GitLabSyncScope.Group), + targetEnvironment: z.string().optional(), + groupId: z.string().trim().min(1, "Group ID required"), + groupName: z.string().trim().min(1, "Group name required"), + shouldProtectSecrets: z.boolean().optional().default(false), + shouldMaskSecrets: z.boolean().optional().default(false), + shouldHideSecrets: z.boolean().optional().default(false) + }) + ]) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index eb01e28c1..5d15492eb 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -12,6 +12,7 @@ import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-s import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; +import { GitlabSyncDestinationSchema } from "./gitlab-sync-destination-schema"; import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema"; import { HerokuSyncDestinationSchema } from "./heroku-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; @@ -43,6 +44,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ HerokuSyncDestinationSchema, RenderSyncDestinationSchema, FlyioSyncDestinationSchema, + GitlabSyncDestinationSchema, CloudflarePagesSyncDestinationSchema ]); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index b723a8417..e1fd9e365 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -24,6 +24,7 @@ import { GcpConnectionMethod, GitHubConnectionMethod, GitHubRadarConnectionMethod, + GitLabConnectionMethod, HCVaultConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, @@ -86,6 +87,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Heroku]: { name: "Heroku", image: "Heroku.png" }, [AppConnection.Render]: { name: "Render", image: "Render.png" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, + [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" }, [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" } }; @@ -100,6 +102,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case AzureDevOpsConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth: case HerokuConnectionMethod.OAuth: + case GitLabConnectionMethod.OAuth: return { name: "OAuth", icon: faPassport }; case AwsConnectionMethod.AccessKey: case OCIConnectionMethod.AccessKey: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 7d75f5859..d6395397d 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -74,6 +74,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Heroku]: AppConnection.Heroku, [SecretSync.Render]: AppConnection.Render, [SecretSync.Flyio]: AppConnection.Flyio, + [SecretSync.GitLab]: AppConnection.Gitlab, [SecretSync.CloudflarePages]: AppConnection.Cloudflare }; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index f64580c16..8097720a7 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -26,5 +26,6 @@ export enum AppConnection { Heroku = "heroku", Render = "render", Flyio = "flyio", + Gitlab = "gitlab", Cloudflare = "cloudflare" } diff --git a/frontend/src/hooks/api/appConnections/gitlab/index.ts b/frontend/src/hooks/api/appConnections/gitlab/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/gitlab/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/gitlab/queries.tsx b/frontend/src/hooks/api/appConnections/gitlab/queries.tsx new file mode 100644 index 000000000..155d56a23 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/gitlab/queries.tsx @@ -0,0 +1,64 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TGitLabGroup, TGitLabProject } from "./types"; + +const gitlabConnectionKeys = { + all: [...appConnectionKeys.all, "gitlab"] as const, + listProjects: (connectionId: string) => + [...gitlabConnectionKeys.all, "projects", connectionId] as const, + listGroups: (connectionId: string) => + [...gitlabConnectionKeys.all, "groups", connectionId] as const +}; + +export const useGitlabConnectionListProjects = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TGitLabProject[], + unknown, + TGitLabProject[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: gitlabConnectionKeys.listProjects(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/gitlab/${connectionId}/projects` + ); + + return data; + }, + ...options + }); +}; + +export const useGitlabConnectionListGroups = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TGitLabGroup[], + unknown, + TGitLabGroup[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: gitlabConnectionKeys.listGroups(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/gitlab/${connectionId}/groups` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/gitlab/types.ts b/frontend/src/hooks/api/appConnections/gitlab/types.ts new file mode 100644 index 000000000..0d8d9baf0 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/gitlab/types.ts @@ -0,0 +1,14 @@ +export type TGitLabProject = { + id: string; + name: string; +}; + +export type TGitLabGroup = { + id: string; + name: string; +}; + +export enum GitLabAccessTokenType { + Personal = "personal", + Project = "project" +} diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 3550a0d9b..b71370da7 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -123,6 +123,11 @@ export type TFlyioConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Flyio; }; +export type TGitlabConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Gitlab; + oauthClientId?: string; +}; + export type TCloudflareConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Cloudflare; }; @@ -153,6 +158,7 @@ export type TAppConnectionOption = | THerokuConnectionOption | TRenderConnectionOption | TFlyioConnectionOption + | TGitlabConnectionOption | TCloudflareConnectionOption; export type TAppConnectionOptionMap = { @@ -183,5 +189,6 @@ export type TAppConnectionOptionMap = { [AppConnection.Heroku]: THerokuConnectionOption; [AppConnection.Render]: TRenderConnectionOption; [AppConnection.Flyio]: TFlyioConnectionOption; + [AppConnection.Gitlab]: TGitlabConnectionOption; [AppConnection.Cloudflare]: TCloudflareConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/gitlab-connection.ts b/frontend/src/hooks/api/appConnections/types/gitlab-connection.ts new file mode 100644 index 000000000..667475126 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/gitlab-connection.ts @@ -0,0 +1,27 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +import { GitLabAccessTokenType } from "../gitlab"; + +export enum GitLabConnectionMethod { + AccessToken = "access-token", + OAuth = "oauth" +} + +export type TGitLabConnection = TRootAppConnection & { app: AppConnection.Gitlab } & ( + | { + method: GitLabConnectionMethod.AccessToken; + credentials: { + instanceUrl?: string; + accessToken: string; + accessTokenType: GitLabAccessTokenType; + }; + } + | { + method: GitLabConnectionMethod.OAuth; + credentials: { + code: string; + instanceUrl?: string; + }; + } + ); diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 5bdc0ac10..2eaebb45a 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -14,6 +14,7 @@ import { TFlyioConnection } from "./flyio-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; import { TGitHubRadarConnection } from "./github-radar-connection"; +import { TGitLabConnection } from "./gitlab-connection"; import { THCVaultConnection } from "./hc-vault-connection"; import { THerokuConnection } from "./heroku-connection"; import { THumanitecConnection } from "./humanitec-connection"; @@ -37,11 +38,13 @@ export * from "./azure-client-secrets-connection"; export * from "./azure-devops-connection"; export * from "./azure-key-vault-connection"; export * from "./camunda-connection"; +export * from "./cloudflare-connection"; export * from "./databricks-connection"; export * from "./flyio-connection"; export * from "./gcp-connection"; export * from "./github-connection"; export * from "./github-radar-connection"; +export * from "./gitlab-connection"; export * from "./hc-vault-connection"; export * from "./heroku-connection"; export * from "./humanitec-connection"; @@ -56,7 +59,6 @@ export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; -export * from "./cloudflare-connection"; export type TAppConnection = | TAwsConnection @@ -86,6 +88,7 @@ export type TAppConnection = | THerokuConnection | TRenderConnection | TFlyioConnection + | TGitLabConnection | TCloudflareConnection; export type TAvailableAppConnection = Pick; @@ -141,5 +144,6 @@ export type TAppConnectionMap = { [AppConnection.Heroku]: THerokuConnection; [AppConnection.Render]: TRenderConnection; [AppConnection.Flyio]: TFlyioConnection; + [AppConnection.Gitlab]: TGitLabConnection; [AppConnection.Cloudflare]: TCloudflareConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 46929b46f..66834ced5 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -19,6 +19,7 @@ export enum SecretSync { Heroku = "heroku", Render = "render", Flyio = "flyio", + GitLab = "gitlab", CloudflarePages = "cloudflare-pages" } diff --git a/frontend/src/hooks/api/secretSyncs/types/gitlab-sync.ts b/frontend/src/hooks/api/secretSyncs/types/gitlab-sync.ts new file mode 100644 index 000000000..ed88ab685 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/gitlab-sync.ts @@ -0,0 +1,36 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export enum GitLabSyncScope { + Project = "project", + Group = "group" +} + +export type TGitLabSync = TRootSecretSync & { + destination: SecretSync.GitLab; + destinationConfig: + | { + scope: GitLabSyncScope.Project; + projectId: string; + projectName: string; + targetEnvironment?: string; + shouldProtectSecrets?: boolean; + shouldMaskSecrets?: boolean; + shouldHideSecrets?: boolean; + } + | { + scope: GitLabSyncScope.Group; + groupId: string; + groupName: string; + targetEnvironment?: string; + shouldProtectSecrets?: boolean; + shouldMaskSecrets?: boolean; + shouldHideSecrets?: boolean; + }; + connection: { + app: AppConnection.Gitlab; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index b55f64b23..3e254e9aa 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -14,6 +14,7 @@ import { TDatabricksSync } from "./databricks-sync"; import { TFlyioSync } from "./flyio-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; +import { TGitLabSync } from "./gitlab-sync"; import { THCVaultSync } from "./hc-vault-sync"; import { THerokuSync } from "./heroku-sync"; import { THumanitecSync } from "./humanitec-sync"; @@ -51,6 +52,7 @@ export type TSecretSync = | THerokuSync | TRenderSync | TFlyioSync + | TGitLabSync | TCloudflarePagesSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 1a90085e4..52abfee5d 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -23,6 +23,7 @@ import { FlyioConnectionForm } from "./FlyioConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; import { GitHubRadarConnectionForm } from "./GitHubRadarConnectionForm"; +import { GitLabConnectionForm } from "./GitLabConnectionForm"; import { HCVaultConnectionForm } from "./HCVaultConnectionForm"; import { HerokuConnectionForm } from "./HerokuAppConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; @@ -129,6 +130,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Flyio: return ; + case AppConnection.Gitlab: + return ; case AppConnection.Cloudflare: return ; default: @@ -221,6 +224,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Flyio: return ; + case AppConnection.Gitlab: + return ; case AppConnection.Cloudflare: return ; default: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx new file mode 100644 index 000000000..038e14ca8 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitLabConnectionForm.tsx @@ -0,0 +1,335 @@ +/* eslint-disable no-case-declarations */ +/* eslint-disable no-nested-ternary */ +import crypto from "crypto"; + +import { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { isInfisicalCloud } from "@app/helpers/platform"; +import { useGetAppConnectionOption } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { GitLabAccessTokenType } from "@app/hooks/api/appConnections/gitlab"; +import { + GitLabConnectionMethod, + TGitLabConnection +} from "@app/hooks/api/appConnections/types/gitlab-connection"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TGitLabConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const formSchema = z.discriminatedUnion("method", [ + genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Gitlab), + method: z.literal(GitLabConnectionMethod.AccessToken), + credentials: z.object({ + accessToken: z.string().min(1, "Access token is required"), + accessTokenType: z.nativeEnum(GitLabAccessTokenType), + instanceUrl: z + .string() + .trim() + .transform((value) => value || undefined) + .refine((value) => (!value ? true : z.string().url().safeParse(value).success), { + message: "Invalid instance URL" + }) + .optional() + }) + }), + genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Gitlab), + method: z.literal(GitLabConnectionMethod.OAuth), + credentials: z.object({ + code: z.string().min(1, "Code is required"), + instanceUrl: z + .string() + .trim() + .transform((value) => value || undefined) + .refine((value) => (!value ? true : z.string().url().safeParse(value).success), { + message: "Invalid instance URL" + }) + .optional() + }) + }) +]); + +type FormData = z.infer; + +export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + const [isRedirecting, setIsRedirecting] = useState(false); + + const { + option: { oauthClientId }, + isLoading + } = useGetAppConnectionOption(AppConnection.Gitlab); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: + appConnection?.method === GitLabConnectionMethod.OAuth + ? { ...appConnection, credentials: { code: "custom" } } + : (appConnection ?? + ({ + app: AppConnection.Gitlab, + method: GitLabConnectionMethod.AccessToken, + credentials: { + accessToken: "", + accessTokenType: GitLabAccessTokenType.Personal, + instanceUrl: "" + } + } as FormData)) + }); + + const { + handleSubmit, + control, + watch, + setValue, + formState: { isSubmitting, isDirty } + } = form; + + const selectedMethod = watch("method"); + const gitLabURL = watch("credentials.instanceUrl"); + + const onSubmit = async (formData: FormData) => { + try { + switch (formData.method) { + case GitLabConnectionMethod.AccessToken: + await formSubmit(formData); + break; + + case GitLabConnectionMethod.OAuth: + if (!oauthClientId) { + return; + } + setIsRedirecting(true); + + // Generate CSRF token + const state = crypto.randomBytes(16).toString("hex"); + + // Store state and form data for callback + localStorage.setItem("latestCSRFToken", state); + localStorage.setItem( + "gitlabConnectionFormData", + JSON.stringify({ + ...formData, + connectionId: appConnection?.id, + isUpdate + }) + ); + + // Redirect to Gitlab OAuth + const baseURL = + gitLabURL && (gitLabURL as string)?.trim() !== "" + ? (gitLabURL as string)?.trim() + : "https://gitlab.com"; + const oauthUrl = new URL(`${baseURL}/oauth/authorize`); + oauthUrl.searchParams.set("client_id", oauthClientId); + oauthUrl.searchParams.set( + "redirect_uri", + `${window.location.origin}/organization/app-connections/gitlab/oauth/callback` + ); + oauthUrl.searchParams.set("response_type", "code"); + oauthUrl.searchParams.set("state", state); + + window.location.assign(oauthUrl.toString()); + break; + + default: + throw new Error("Unhandled GitLab Connection method"); + } + } catch (error) { + console.error("Error handling form submission:", error); + setIsRedirecting(false); + } + }; + + let isMissingConfig: boolean; + + switch (selectedMethod) { + case GitLabConnectionMethod.OAuth: + isMissingConfig = !oauthClientId; + break; + case GitLabConnectionMethod.AccessToken: + isMissingConfig = false; + break; + default: + throw new Error(`Unhandled GitLab Connection method: ${selectedMethod}`); + } + + const methodDetails = getAppConnectionMethodDetails(selectedMethod); + + return ( + +
+ {!isUpdate && } + + ( + + onChange(e.target.value)} + placeholder="https://gitlab.com" + /> + + )} + /> + + ( + + + + )} + /> + + {selectedMethod === GitLabConnectionMethod.AccessToken && ( + <> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> + + )} + +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx index ddce30f03..2bb6b3452 100644 --- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx @@ -11,12 +11,14 @@ import { AzureDevOpsConnectionMethod, AzureKeyVaultConnectionMethod, GitHubConnectionMethod, + GitLabConnectionMethod, TAzureAppConfigurationConnection, TAzureClientSecretsConnection, TAzureDevOpsConnection, TAzureKeyVaultConnection, TGitHubConnection, TGitHubRadarConnection, + TGitLabConnection, useCreateAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections"; @@ -25,6 +27,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; type BaseFormData = { returnUrl?: string; connectionId?: string; + isUpdate?: boolean; }; type GithubFormData = BaseFormData & Pick; @@ -32,6 +35,8 @@ type GithubFormData = BaseFormData & Pick; +type GitLabFormData = BaseFormData & Pick; + type AzureKeyVaultFormData = BaseFormData & Pick & Pick; @@ -60,6 +65,7 @@ type AzureDevOpsFormData = BaseFormData & type FormDataMap = { [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub }; [AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar }; + [AppConnection.Gitlab]: GitLabFormData & { app: AppConnection.Gitlab }; [AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault }; [AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & { app: AppConnection.AzureAppConfiguration; @@ -75,6 +81,7 @@ type FormDataMap = { const formDataStorageFieldMap: Partial> = { [AppConnection.GitHub]: "githubConnectionFormData", [AppConnection.GitHubRadar]: "githubRadarConnectionFormData", + [AppConnection.Gitlab]: "gitlabConnectionFormData", [AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData", [AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData", [AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData", @@ -133,6 +140,57 @@ export const OAuthCallbackPage = () => { } }; + const handleGitlab = useCallback(async () => { + const formData = getFormData(AppConnection.Gitlab); + if (formData === null) return null; + + clearState(AppConnection.Gitlab); + + const { connectionId, name, description, returnUrl, isUpdate } = formData; + + try { + if (isUpdate && connectionId) { + await updateAppConnection.mutateAsync({ + app: AppConnection.Gitlab, + connectionId, + credentials: { + code: code as string + } + }); + } else { + await createAppConnection.mutateAsync({ + app: AppConnection.Gitlab, + name, + description, + method: GitLabConnectionMethod.OAuth, + credentials: { + code: code as string + } + }); + } + + navigate({ + to: returnUrl ?? "/organization/app-connections" + }); + + return { + connectionId, + returnUrl, + appConnectionName: formData.app + }; + } catch (err: any) { + createNotification({ + title: `Failed to ${connectionId ? "update" : "add"} GitLab Connection`, + text: err?.message, + type: "error" + }); + navigate({ + to: returnUrl ?? "/organization/app-connections" + }); + return null; + } + }, []); + const handleAzureKeyVault = useCallback(async () => { const formData = getFormData(AppConnection.AzureKeyVault); if (formData === null) return null; @@ -463,6 +521,8 @@ export const OAuthCallbackPage = () => { data = await handleGithub(); } else if (appConnection === AppConnection.GitHubRadar) { data = await handleGithubRadar(); + } else if (appConnection === AppConnection.Gitlab) { + data = await handleGitlab(); } else if (appConnection === AppConnection.AzureKeyVault) { data = await handleAzureKeyVault(); } else if (appConnection === AppConnection.AzureAppConfiguration) { diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/GitLabSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/GitLabSyncDestinationCol.tsx new file mode 100644 index 000000000..9eb86be67 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/GitLabSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TGitLabSync } from "@app/hooks/api/secretSyncs/types/gitlab-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TGitLabSync; +}; + +export const GitLabSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 5eabb7edf..01064ef1d 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -12,6 +12,7 @@ import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; +import { GitLabSyncDestinationCol } from "./GitLabSyncDestinationCol"; import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol"; import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; @@ -68,6 +69,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Flyio: return ; + case SecretSync.GitLab: + return ; case SecretSync.CloudflarePages: return ; default: diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 54638b96b..26c9144d7 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -5,6 +5,7 @@ import { GitHubSyncScope, GitHubSyncVisibility } from "@app/hooks/api/secretSyncs/types/github-sync"; +import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync"; import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; // This functional ensures parity across what is displayed in the destination column @@ -128,6 +129,17 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.appId; secondaryText = "App ID"; break; + case SecretSync.GitLab: + if (destinationConfig.scope === GitLabSyncScope.Project) { + primaryText = destinationConfig.projectName; + secondaryText = destinationConfig.projectId; + } else if (destinationConfig.scope === GitLabSyncScope.Group) { + primaryText = destinationConfig.groupName; + secondaryText = destinationConfig.groupId; + } else { + throw new Error(`Unhandled GitLab Scope Destination Col Values ${destination}`); + } + break; case SecretSync.CloudflarePages: primaryText = destinationConfig.projectName; secondaryText = destinationConfig.environment; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitLabSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitLabSyncDestinationSection.tsx new file mode 100644 index 000000000..11ded2752 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitLabSyncDestinationSection.tsx @@ -0,0 +1,50 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { GitLabSyncScope, TGitLabSync } from "@app/hooks/api/secretSyncs/types/gitlab-sync"; + +type Props = { + secretSync: TGitLabSync; +}; + +export const GitLabSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { + targetEnvironment, + shouldProtectSecrets, + shouldMaskSecrets, + shouldHideSecrets + } + } = secretSync; + + return ( + <> + {secretSync.destinationConfig.scope === GitLabSyncScope.Project && ( + <> + + {secretSync.destinationConfig.projectName} + + + {secretSync.destinationConfig.projectId} + + + )} + {secretSync.destinationConfig.scope === GitLabSyncScope.Group && ( + <> + + {secretSync.destinationConfig.groupName} + + + {secretSync.destinationConfig.groupId} + + + )} + {targetEnvironment && ( + {targetEnvironment} + )} + + {shouldProtectSecrets ? "Yes" : "No"} + + {shouldMaskSecrets ? "Yes" : "No"} + {shouldHideSecrets ? "Yes" : "No"} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 10b5f1164..f49b75b52 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -23,6 +23,7 @@ import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSec import { FlyioSyncDestinationSection } from "./FlyioSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection"; +import { GitLabSyncDestinationSection } from "./GitLabSyncDestinationSection"; import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection"; import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; @@ -107,6 +108,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Flyio: DestinationComponents = ; break; + case SecretSync.GitLab: + DestinationComponents = ; + break; case SecretSync.CloudflarePages: DestinationComponents = ; break; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index e560196fc..a8406d13d 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -58,6 +58,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Heroku: case SecretSync.Render: case SecretSync.Flyio: + case SecretSync.GitLab: case SecretSync.CloudflarePages: AdditionalSyncOptionsComponent = null; break; diff --git a/frontend/src/pages/secret-manager/integrations/GitlabOauthCallbackPage/GitlabOauthCallbackPage.tsx b/frontend/src/pages/secret-manager/integrations/GitlabOauthCallbackPage/GitlabOauthCallbackPage.tsx index bf7230146..c53cfd628 100644 --- a/frontend/src/pages/secret-manager/integrations/GitlabOauthCallbackPage/GitlabOauthCallbackPage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GitlabOauthCallbackPage/GitlabOauthCallbackPage.tsx @@ -3,11 +3,13 @@ import { useNavigate, useSearch } from "@tanstack/react-router"; import { ROUTE_PATHS } from "@app/const/routes"; import { useWorkspace } from "@app/context"; -import { useAuthorizeIntegration } from "@app/hooks/api"; +import { useCreateAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections"; +import { GitLabConnectionMethod } from "@app/hooks/api/appConnections/types/gitlab-connection"; export const GitLabOAuthCallbackPage = () => { const navigate = useNavigate(); - const { mutateAsync } = useAuthorizeIntegration(); + const { mutateAsync: createAppConnection } = useCreateAppConnection(); + const { mutateAsync: updateAppConnection } = useUpdateAppConnection(); const { code, state } = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.GitlabOauthCallbackPage.id @@ -17,37 +19,82 @@ export const GitLabOAuthCallbackPage = () => { useEffect(() => { (async () => { try { - // validate state - const [csrfToken, url] = (state as string).split("|", 2); + // Validate CSRF state token + const [csrfToken] = (state as string).split("|", 2); + const storedState = localStorage.getItem("latestCSRFToken"); + if (csrfToken !== storedState) { + console.error("CSRF token mismatch"); + navigate({ + to: "/organization/app-connections", + search: { error: "invalid_state" } + }); + return; + } - if (csrfToken !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - const integrationAuth = await mutateAsync({ - workspaceId: currentWorkspace.id, - code: code as string, - integration: "gitlab", - ...(url === "" - ? {} - : { - url - }) - }); + const storedFormData = localStorage.getItem("gitlabConnectionFormData"); + if (!storedFormData) { + console.error("No stored form data found"); + navigate({ + to: "/organization/app-connections", + search: { error: "missing_form_data" } + }); + return; + } + + const formData = JSON.parse(storedFormData); + localStorage.removeItem("gitlabConnectionFormData"); + + // Prepare app connection data with OAuth credentials + const connectionData = { + ...formData, + method: GitLabConnectionMethod.OAuth, + credentials: { + code: code as string + } + }; + + let appConnection; + + // Create or update app connection + if (formData.isUpdate && formData.connectionId) { + appConnection = await updateAppConnection({ + connectionId: formData.connectionId, + ...connectionData + }); + } else { + appConnection = await createAppConnection({ + workspaceId: currentWorkspace.id, + ...connectionData + }); + } + + // Navigate to success page or app connections list navigate({ - to: "/secret-manager/$projectId/integrations/gitlab/create", - params: { - projectId: currentWorkspace.id - }, + to: "/organization/app-connections", search: { - integrationAuthId: integrationAuth.id + success: formData.isUpdate ? "connection_updated" : "connection_created", + connectionId: appConnection.id } }); } catch (err) { - console.error(err); + console.error("Error handling GitLab OAuth callback:", err); + navigate({ + to: "/organization/app-connections", + search: { error: "connection_failed" } + }); } })(); - }, []); + }, [code, state, navigate, createAppConnection, updateAppConnection, currentWorkspace.id]); - return
; + return ( +
+
+
+

Connecting to GitLab...

+
+
+ ); };