mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 22:28:15 +00:00
feat(secret-sync): Add gitlab secret sync
This commit is contained in:
@@ -2228,6 +2228,11 @@ 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."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2401,6 +2406,16 @@ export const SecretSyncs = {
|
||||
},
|
||||
FLYIO: {
|
||||
appId: "The ID of the Fly.io app to sync secrets to."
|
||||
},
|
||||
GITLAB: {
|
||||
projectId: "The GitLab project to sync secrets to.",
|
||||
projectName: "The GitLab project name to sync secrets to.",
|
||||
groupId: "The GitLab group to sync secrets to.",
|
||||
scope: "The GitLab project scope that secrets should be synced to. (default: individual)",
|
||||
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"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
GitHubRadarConnectionListItemSchema,
|
||||
SanitizedGitHubRadarConnectionSchema
|
||||
} from "@app/services/app-connection/github-radar";
|
||||
import { GitLabConnectionListItemSchema, SanitizedGitLabConnectionSchema } from "@app/services/app-connection/gitlab";
|
||||
import {
|
||||
HCVaultConnectionListItemSchema,
|
||||
SanitizedHCVaultConnectionSchema
|
||||
@@ -109,7 +110,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedOnePassConnectionSchema.options,
|
||||
...SanitizedHerokuConnectionSchema.options,
|
||||
...SanitizedRenderConnectionSchema.options,
|
||||
...SanitizedFlyioConnectionSchema.options
|
||||
...SanitizedFlyioConnectionSchema.options,
|
||||
...SanitizedGitLabConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -139,7 +141,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
OnePassConnectionListItemSchema,
|
||||
HerokuConnectionListItemSchema,
|
||||
RenderConnectionListItemSchema,
|
||||
FlyioConnectionListItemSchema
|
||||
FlyioConnectionListItemSchema,
|
||||
GitLabConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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()
|
||||
}),
|
||||
querystring: z.object({
|
||||
group: z.string().optional()
|
||||
}),
|
||||
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,
|
||||
req.query.group
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -15,6 +15,7 @@ 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";
|
||||
@@ -58,5 +59,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.OnePass]: registerOnePassConnectionRouter,
|
||||
[AppConnection.Heroku]: registerHerokuConnectionRouter,
|
||||
[AppConnection.Render]: registerRenderConnectionRouter,
|
||||
[AppConnection.Flyio]: registerFlyioConnectionRouter
|
||||
[AppConnection.Flyio]: registerFlyioConnectionRouter,
|
||||
[AppConnection.GitLab]: registerGitLabConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CreateGitLabSyncSchema, GitLabSyncSchema, UpdateGitLabSyncSchema } from "@app/services/secret-sync/gitlab";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerGitLabSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.GitLab,
|
||||
server,
|
||||
responseSchema: GitLabSyncSchema,
|
||||
createSchema: CreateGitLabSyncSchema,
|
||||
updateSchema: UpdateGitLabSyncSchema
|
||||
});
|
||||
@@ -12,6 +12,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";
|
||||
@@ -43,5 +44,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.OnePass]: registerOnePassSyncRouter,
|
||||
[SecretSync.Heroku]: registerHerokuSyncRouter,
|
||||
[SecretSync.Render]: registerRenderSyncRouter,
|
||||
[SecretSync.Flyio]: registerFlyioSyncRouter
|
||||
[SecretSync.Flyio]: registerFlyioSyncRouter,
|
||||
[SecretSync.GitLab]: registerGitLabSyncRouter
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
|
||||
import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio";
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { GitLabSyncListItemSchema, GitLabSyncSchema } from "@app/services/secret-sync/gitlab";
|
||||
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
|
||||
import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
@@ -55,7 +56,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
OnePassSyncSchema,
|
||||
HerokuSyncSchema,
|
||||
RenderSyncSchema,
|
||||
FlyioSyncSchema
|
||||
FlyioSyncSchema,
|
||||
GitLabSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -78,7 +80,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
OnePassSyncListItemSchema,
|
||||
HerokuSyncListItemSchema,
|
||||
RenderSyncListItemSchema,
|
||||
FlyioSyncListItemSchema
|
||||
FlyioSyncListItemSchema,
|
||||
GitLabSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -25,7 +25,8 @@ export enum AppConnection {
|
||||
OnePass = "1password",
|
||||
Heroku = "heroku",
|
||||
Render = "render",
|
||||
Flyio = "flyio"
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
GitHubRadarConnectionMethod,
|
||||
validateGitHubRadarConnectionCredentials
|
||||
} from "./github-radar";
|
||||
import { getGitLabConnectionListItem, GitLabConnectionMethod, validateGitLabConnectionCredentials } from "./gitlab";
|
||||
import {
|
||||
getHCVaultConnectionListItem,
|
||||
HCVaultConnectionMethod,
|
||||
@@ -128,7 +129,8 @@ export const listAppConnectionOptions = () => {
|
||||
getOnePassConnectionListItem(),
|
||||
getHerokuConnectionListItem(),
|
||||
getRenderConnectionListItem(),
|
||||
getFlyioConnectionListItem()
|
||||
getFlyioConnectionListItem(),
|
||||
getGitLabConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -206,7 +208,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@@ -223,6 +226,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";
|
||||
@@ -318,7 +322,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.OnePass]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Heroku]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Render]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitLab]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
export const enterpriseAppCheck = async (
|
||||
|
||||
@@ -27,7 +27,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.OnePass]: "1Password",
|
||||
[AppConnection.Heroku]: "Heroku",
|
||||
[AppConnection.Render]: "Render",
|
||||
[AppConnection.Flyio]: "Fly.io"
|
||||
[AppConnection.Flyio]: "Fly.io",
|
||||
[AppConnection.GitLab]: "GitLab"
|
||||
};
|
||||
|
||||
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -57,5 +58,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.MySql]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Heroku]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Render]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -56,6 +56,8 @@ import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar";
|
||||
import { ValidateGitLabConnectionCredentialsSchema } from "./gitlab";
|
||||
import { gitlabConnectionService } from "./gitlab/gitlab-connection-service";
|
||||
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
|
||||
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
|
||||
import { ValidateHerokuConnectionCredentialsSchema } from "./heroku";
|
||||
@@ -113,7 +115,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema,
|
||||
[AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema,
|
||||
[AppConnection.Render]: ValidateRenderConnectionCredentialsSchema,
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -521,6 +524,7 @@ export const appConnectionServiceFactory = ({
|
||||
onepass: onePassConnectionService(connectAppConnectionById),
|
||||
heroku: herokuConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
render: renderConnectionService(connectAppConnectionById),
|
||||
flyio: flyioConnectionService(connectAppConnectionById)
|
||||
flyio: flyioConnectionService(connectAppConnectionById),
|
||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -92,6 +92,12 @@ import {
|
||||
TGitHubRadarConnectionInput,
|
||||
TValidateGitHubRadarConnectionCredentialsSchema
|
||||
} from "./github-radar";
|
||||
import {
|
||||
TGitLabConnection,
|
||||
TGitLabConnectionConfig,
|
||||
TGitLabConnectionInput,
|
||||
TValidateGitLabConnectionCredentialsSchema
|
||||
} from "./gitlab";
|
||||
import {
|
||||
THCVaultConnection,
|
||||
THCVaultConnectionConfig,
|
||||
@@ -182,6 +188,7 @@ export type TAppConnection = { id: string } & (
|
||||
| THerokuConnection
|
||||
| TRenderConnection
|
||||
| TFlyioConnection
|
||||
| TGitLabConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -216,6 +223,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| THerokuConnectionInput
|
||||
| TRenderConnectionInput
|
||||
| TFlyioConnectionInput
|
||||
| TGitLabConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -257,7 +265,8 @@ export type TAppConnectionConfig =
|
||||
| TOnePassConnectionConfig
|
||||
| THerokuConnectionConfig
|
||||
| TRenderConnectionConfig
|
||||
| TFlyioConnectionConfig;
|
||||
| TFlyioConnectionConfig
|
||||
| TGitLabConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -286,7 +295,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateOnePassConnectionCredentialsSchema
|
||||
| TValidateHerokuConnectionCredentialsSchema
|
||||
| TValidateRenderConnectionCredentialsSchema
|
||||
| TValidateFlyioConnectionCredentialsSchema;
|
||||
| TValidateFlyioConnectionCredentialsSchema
|
||||
| TValidateGitLabConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum GitLabConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
AccessToken = "access-token"
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||
import { GitLabConnectionMethod } from "./gitlab-connection-enums";
|
||||
import { TGitLabConnection, TGitLabConnectionConfig, TGitLabGroup, TGitLabProject } from "./gitlab-connection-types";
|
||||
|
||||
interface GitLabOAuthTokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
created_at: number;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export const getGitLabConnectionListItem = () => {
|
||||
const { CLIENT_ID_GITLAB_LOGIN } = getConfig();
|
||||
|
||||
return {
|
||||
name: "GitLab" as const,
|
||||
app: AppConnection.GitLab as const,
|
||||
methods: Object.values(GitLabConnectionMethod) as [
|
||||
GitLabConnectionMethod.AccessToken,
|
||||
GitLabConnectionMethod.OAuth
|
||||
],
|
||||
oauthClientId: CLIENT_ID_GITLAB_LOGIN
|
||||
};
|
||||
};
|
||||
|
||||
export const refreshGitLabToken = async (
|
||||
refreshToken: string,
|
||||
appId: string,
|
||||
orgId: string,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
|
||||
instanceUrl?: string
|
||||
): Promise<string> => {
|
||||
const { CLIENT_ID_GITLAB_LOGIN, CLIENT_SECRET_GITLAB_LOGIN, SITE_URL } = getConfig();
|
||||
if (!CLIENT_SECRET_GITLAB_LOGIN || !CLIENT_ID_GITLAB_LOGIN || !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: CLIENT_ID_GITLAB_LOGIN,
|
||||
client_secret: CLIENT_SECRET_GITLAB_LOGIN,
|
||||
redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback`
|
||||
});
|
||||
|
||||
try {
|
||||
const { data } = await request.post<GitLabOAuthTokenResponse>(
|
||||
`${instanceUrl ? `${instanceUrl}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL}`,
|
||||
payload.toString(),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const expiresAt = new Date(Date.now() + data.expires_in * 1000 - 60000);
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: {
|
||||
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<GitLabOAuthTokenResponse> => {
|
||||
const { CLIENT_ID_GITLAB_LOGIN, CLIENT_SECRET_GITLAB_LOGIN, SITE_URL } = getConfig();
|
||||
if (!CLIENT_SECRET_GITLAB_LOGIN || !CLIENT_ID_GITLAB_LOGIN || !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: CLIENT_ID_GITLAB_LOGIN,
|
||||
client_secret: CLIENT_SECRET_GITLAB_LOGIN,
|
||||
redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback`
|
||||
});
|
||||
|
||||
const response = await request.post<GitLabOAuthTokenResponse>(
|
||||
instanceUrl ? `${instanceUrl}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL,
|
||||
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"
|
||||
});
|
||||
}
|
||||
|
||||
let response: AxiosResponse<TGitLabProject[]> | null = null;
|
||||
|
||||
try {
|
||||
response = await request.get<TGitLabProject[]>(
|
||||
`${inputCredentials.instanceUrl ? `${inputCredentials.instanceUrl}/api` : IntegrationUrls.GITLAB_API_URL}/v4/groups`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
if (!response?.data) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to validate credentials: Response was empty"
|
||||
});
|
||||
}
|
||||
|
||||
if (method === GitLabConnectionMethod.OAuth && oauthData) {
|
||||
return {
|
||||
accessToken,
|
||||
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,
|
||||
teamId
|
||||
}: {
|
||||
appConnection: TGitLabConnection;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
teamId?: string;
|
||||
}): Promise<TGitLabProject[]> => {
|
||||
let { accessToken } = appConnection.credentials;
|
||||
|
||||
if (
|
||||
appConnection.method === GitLabConnectionMethod.OAuth &&
|
||||
appConnection.credentials.refreshToken &&
|
||||
appConnection.credentials.expiresAt < new Date()
|
||||
) {
|
||||
accessToken = await refreshGitLabToken(
|
||||
appConnection.credentials.refreshToken,
|
||||
appConnection.id,
|
||||
appConnection.orgId,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
appConnection.credentials.instanceUrl
|
||||
);
|
||||
}
|
||||
|
||||
const gitLabApiUrl = appConnection.credentials.instanceUrl
|
||||
? `${appConnection.credentials.instanceUrl}/api/v4`
|
||||
: `${IntegrationUrls.GITLAB_API_URL}/v4`;
|
||||
|
||||
const projects: TGitLabProject[] = [];
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
let hasMorePages = true;
|
||||
|
||||
try {
|
||||
if (teamId) {
|
||||
while (hasMorePages) {
|
||||
const { data } = await request.get<TGitLabProject[]>(`${gitLabApiUrl}/groups/${teamId}/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
page: page.toString(),
|
||||
per_page: perPage.toString(),
|
||||
order_by: "updated_at",
|
||||
sort: "desc",
|
||||
include_subgroups: "true"
|
||||
}
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get group projects: Response was empty"
|
||||
});
|
||||
}
|
||||
|
||||
data.forEach((project) => {
|
||||
projects.push({
|
||||
name: project.name,
|
||||
id: project.id.toString()
|
||||
});
|
||||
});
|
||||
|
||||
hasMorePages = data.length === perPage;
|
||||
page += 1;
|
||||
}
|
||||
} else {
|
||||
const { data: userData } = await request.get<{ id: string }>(`${gitLabApiUrl}/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!userData?.id) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get current user information"
|
||||
});
|
||||
}
|
||||
|
||||
while (hasMorePages) {
|
||||
const { data } = await request.get<TGitLabProject[]>(`${gitLabApiUrl}/users/${userData.id}/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
page: page.toString(),
|
||||
per_page: perPage.toString(),
|
||||
order_by: "updated_at",
|
||||
sort: "desc"
|
||||
}
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get user projects: Response was empty"
|
||||
});
|
||||
}
|
||||
|
||||
data.forEach((project) => {
|
||||
projects.push({
|
||||
name: project.name,
|
||||
id: project.id.toString()
|
||||
});
|
||||
});
|
||||
|
||||
hasMorePages = data.length === perPage;
|
||||
page += 1;
|
||||
}
|
||||
|
||||
if (projects.length === 0 && appConnection.method === GitLabConnectionMethod.AccessToken) {
|
||||
try {
|
||||
const { data: tokenAssociations } = await request.get<{
|
||||
projects?: TGitLabProject[];
|
||||
groups?: Array<{ projects?: TGitLabProject[] }>;
|
||||
}>(`${gitLabApiUrl}/personal_access_tokens/self/associations`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
min_access_level: "50"
|
||||
}
|
||||
});
|
||||
|
||||
if (tokenAssociations?.projects) {
|
||||
tokenAssociations.projects.forEach((project) => {
|
||||
projects.push({
|
||||
name: project.name,
|
||||
id: project.id.toString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (tokenAssociations?.groups) {
|
||||
tokenAssociations.groups.forEach((group) => {
|
||||
if (group.projects) {
|
||||
group.projects.forEach((project) => {
|
||||
const existingProject = projects.find((p) => p.id === project.id.toString());
|
||||
if (!existingProject) {
|
||||
projects.push({
|
||||
name: project.name,
|
||||
id: project.id.toString()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(error, "Failed to fetch projects via personal access token associations:");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return projects;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
const status = error.response?.status;
|
||||
const { message } = error;
|
||||
|
||||
if (status === 401) {
|
||||
throw new BadRequestError({
|
||||
message: `GitLab authentication failed: ${message}`
|
||||
});
|
||||
} else if (status === 403) {
|
||||
throw new BadRequestError({
|
||||
message: `GitLab access forbidden: ${message}`
|
||||
});
|
||||
} else if (status === 404) {
|
||||
throw new BadRequestError({
|
||||
message: teamId ? `GitLab group not found or access denied: ${message}` : `GitLab user not found: ${message}`
|
||||
});
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to fetch GitLab projects: ${message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof InternalServerError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: "Unable to fetch GitLab projects"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listGitLabGroups = async ({
|
||||
appConnection,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
includeSubgroups = true,
|
||||
owned = false
|
||||
}: {
|
||||
appConnection: TGitLabConnection;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
includeSubgroups?: boolean;
|
||||
owned?: boolean;
|
||||
}): Promise<TGitLabGroup[]> => {
|
||||
let { accessToken } = appConnection.credentials;
|
||||
|
||||
if (
|
||||
appConnection.method === GitLabConnectionMethod.OAuth &&
|
||||
appConnection.credentials.refreshToken &&
|
||||
appConnection.credentials.expiresAt < new Date()
|
||||
) {
|
||||
accessToken = await refreshGitLabToken(
|
||||
appConnection.credentials.refreshToken,
|
||||
appConnection.id,
|
||||
appConnection.orgId,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
appConnection.credentials.instanceUrl
|
||||
);
|
||||
}
|
||||
|
||||
const gitLabApiUrl = appConnection.credentials.instanceUrl
|
||||
? `${appConnection.credentials.instanceUrl}/api/v4`
|
||||
: `${IntegrationUrls.GITLAB_API_URL}/v4`;
|
||||
|
||||
const groups: TGitLabGroup[] = [];
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
let hasMorePages = true;
|
||||
|
||||
try {
|
||||
while (hasMorePages) {
|
||||
const { data } = await request.get<TGitLabGroup[]>(`${gitLabApiUrl}/groups`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
page: page.toString(),
|
||||
per_page: perPage.toString(),
|
||||
order_by: "name",
|
||||
sort: "asc",
|
||||
all_available: (!owned).toString(),
|
||||
owned: owned.toString(),
|
||||
min_access_level: "10",
|
||||
...(includeSubgroups && { with_custom_attributes: "true" })
|
||||
}
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get groups: Response was empty"
|
||||
});
|
||||
}
|
||||
|
||||
data.forEach((group) => {
|
||||
groups.push({
|
||||
id: group.id.toString(),
|
||||
name: group.name
|
||||
});
|
||||
});
|
||||
|
||||
hasMorePages = data.length === perPage;
|
||||
page += 1;
|
||||
}
|
||||
|
||||
return groups;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
const status = error.response?.status;
|
||||
const { message } = error;
|
||||
|
||||
if (status === 401) {
|
||||
throw new BadRequestError({
|
||||
message: `GitLab authentication failed: ${message}`
|
||||
});
|
||||
} else if (status === 403) {
|
||||
throw new BadRequestError({
|
||||
message: `GitLab access forbidden: ${message}`
|
||||
});
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to fetch GitLab groups: ${message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof InternalServerError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: "Unable to fetch GitLab groups"
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
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 { GitLabConnectionMethod } from "./gitlab-connection-enums";
|
||||
|
||||
// Fixed: Use consistent accessToken naming throughout
|
||||
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)
|
||||
});
|
||||
|
||||
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)
|
||||
});
|
||||
|
||||
// Fixed: Updated schema to match GitLab's actual OAuth response structure
|
||||
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)
|
||||
});
|
||||
|
||||
// Schema for refresh token input during initial setup
|
||||
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
|
||||
}) // Don't expose sensitive data
|
||||
}),
|
||||
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()
|
||||
});
|
||||
@@ -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<TGitLabConnection>;
|
||||
|
||||
export const gitlabConnectionService = (
|
||||
getAppConnection: TGetAppConnectionFunc,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const listProjects = async (connectionId: string, actor: OrgServiceActor, teamId?: string) => {
|
||||
try {
|
||||
const appConnection = await getAppConnection(AppConnection.GitLab, connectionId, actor);
|
||||
const projects = await listGitLabProjects({ appConnection, appConnectionDAL, kmsService, teamId });
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -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<typeof GitLabConnectionSchema>;
|
||||
|
||||
export type TGitLabConnectionInput = z.infer<typeof CreateGitLabConnectionSchema> & {
|
||||
app: AppConnection.GitLab;
|
||||
};
|
||||
|
||||
export type TValidateGitLabConnectionCredentialsSchema = typeof ValidateGitLabConnectionCredentialsSchema;
|
||||
|
||||
export type TGitLabConnectionConfig = DiscriminativePick<TGitLabConnectionInput, "method" | "app" | "credentials"> & {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./gitlab-connection-enums";
|
||||
export * from "./gitlab-connection-fns";
|
||||
export * from "./gitlab-connection-schemas";
|
||||
export * from "./gitlab-connection-types";
|
||||
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum GitLabSyncScope {
|
||||
Individual = "individual",
|
||||
Group = "group"
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { GitLabConnectionMethod, refreshGitLabToken, TGitLabConnection } from "@app/services/app-connection/gitlab";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
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";
|
||||
|
||||
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<TGitLabVariablePayload, "key"> {}
|
||||
|
||||
type TGitLabSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const getValidAccessToken = async (
|
||||
connection: TGitLabConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<string> => {
|
||||
if (
|
||||
connection.method === GitLabConnectionMethod.OAuth &&
|
||||
connection.credentials.refreshToken &&
|
||||
connection.credentials.expiresAt < new Date()
|
||||
) {
|
||||
const accessToken = await refreshGitLabToken(
|
||||
connection.credentials.refreshToken,
|
||||
connection.id,
|
||||
connection.orgId,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
);
|
||||
return accessToken;
|
||||
}
|
||||
return connection.credentials.accessToken;
|
||||
};
|
||||
|
||||
const getGitLabApiUrl = (connection: TGitLabConnection): string => {
|
||||
const baseUrl = connection.credentials.instanceUrl || IntegrationUrls.GITLAB_API_URL;
|
||||
return baseUrl.includes("/api") ? baseUrl : `${baseUrl}/api`;
|
||||
};
|
||||
|
||||
const buildVariablesEndpoint = (apiUrl: string, projectId: string): string => {
|
||||
return `${apiUrl}/v4/projects/${encodeURIComponent(projectId)}/variables`;
|
||||
};
|
||||
|
||||
const getGitLabVariables = async ({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
targetEnvironment
|
||||
}: {
|
||||
accessToken: string;
|
||||
connection: TGitLabConnection;
|
||||
projectId: string;
|
||||
targetEnvironment?: string;
|
||||
}): Promise<TGitLabVariable[]> => {
|
||||
try {
|
||||
const apiUrl = getGitLabApiUrl(connection);
|
||||
const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId);
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
let allVariables: TGitLabVariable[] = [];
|
||||
let url: string | null = `${baseEndpoint}?per_page=100`;
|
||||
|
||||
if (targetEnvironment) {
|
||||
url += `&filter[environment_scope]=${encodeURIComponent(targetEnvironment)}`;
|
||||
}
|
||||
|
||||
while (url) {
|
||||
const response = await request.get<TGitLabVariable[]>(url, { headers });
|
||||
allVariables = [...allVariables, ...(response.data || [])];
|
||||
|
||||
const linkHeader = response.headers.link as string;
|
||||
const nextLink = linkHeader?.split(",").find((part: string) => part.includes('rel="next"'));
|
||||
|
||||
if (nextLink) {
|
||||
url = nextLink.trim().split(";")[0].slice(1, -1);
|
||||
} else {
|
||||
url = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetEnvironment) {
|
||||
return allVariables.filter((variable) => variable.environment_scope === targetEnvironment);
|
||||
}
|
||||
|
||||
return allVariables;
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: "list_variables"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createGitLabVariable = async ({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
variable
|
||||
}: {
|
||||
accessToken: string;
|
||||
connection: TGitLabConnection;
|
||||
projectId: string;
|
||||
variable: TGitLabVariableCreate;
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const apiUrl = getGitLabApiUrl(connection);
|
||||
const endpoint = buildVariablesEndpoint(apiUrl, projectId);
|
||||
|
||||
const payload = {
|
||||
key: variable.key,
|
||||
value: variable.value,
|
||||
variable_type: variable.variable_type || "env_var",
|
||||
environment_scope: variable.environment_scope || "*",
|
||||
protected: variable.protected || false,
|
||||
masked: variable.masked || false,
|
||||
masked_and_hidden: variable.masked_and_hidden || false,
|
||||
raw: false,
|
||||
...(variable.description && { description: variable.description })
|
||||
};
|
||||
|
||||
await request.post(endpoint, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: variable.key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateGitLabVariable = async ({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
key,
|
||||
variable,
|
||||
targetEnvironment
|
||||
}: {
|
||||
accessToken: string;
|
||||
connection: TGitLabConnection;
|
||||
projectId: string;
|
||||
key: string;
|
||||
variable: TGitLabVariableUpdate;
|
||||
targetEnvironment?: string;
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const apiUrl = getGitLabApiUrl(connection);
|
||||
const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId);
|
||||
let url = `${baseEndpoint}/${encodeURIComponent(key)}`;
|
||||
|
||||
if (targetEnvironment) {
|
||||
url += `?filter[environment_scope]=${encodeURIComponent(targetEnvironment)}`;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
value: variable.value,
|
||||
...(variable.variable_type && { variable_type: variable.variable_type }),
|
||||
...(variable.environment_scope && { environment_scope: variable.environment_scope }),
|
||||
...(variable.protected !== undefined && { protected: variable.protected }),
|
||||
...(variable.masked !== undefined && { masked: variable.masked }),
|
||||
...(variable.masked_and_hidden !== undefined && { masked_and_hidden: variable.masked_and_hidden }),
|
||||
...(variable.description !== undefined && { description: variable.description || "" })
|
||||
};
|
||||
|
||||
await request.put(url, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteGitLabVariable = async ({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
key,
|
||||
targetEnvironment
|
||||
}: {
|
||||
accessToken: string;
|
||||
connection: TGitLabConnection;
|
||||
projectId: string;
|
||||
key: string;
|
||||
targetEnvironment?: string;
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const apiUrl = getGitLabApiUrl(connection);
|
||||
const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId);
|
||||
let url = `${baseEndpoint}/${encodeURIComponent(key)}`;
|
||||
|
||||
if (targetEnvironment) {
|
||||
url += `?filter[environment_scope]=${encodeURIComponent(targetEnvironment)}`;
|
||||
}
|
||||
|
||||
await request.delete(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
throw new SecretSyncError({
|
||||
error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const GitLabSyncFns = {
|
||||
syncSecrets: async (
|
||||
secretSync: TGitLabSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
{ appConnectionDAL, kmsService }: TGitLabSyncFactoryDeps
|
||||
): Promise<void> => {
|
||||
const { connection, environment, destinationConfig } = secretSync;
|
||||
|
||||
const { projectId, targetEnvironment } = destinationConfig;
|
||||
|
||||
const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService);
|
||||
|
||||
try {
|
||||
const currentVariables = await getGitLabVariables({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
targetEnvironment
|
||||
});
|
||||
|
||||
const currentVariableMap = new Map(currentVariables.map((v) => [v.key, v]));
|
||||
|
||||
for (const [key, { value }] of Object.entries(secretMap)) {
|
||||
const existingVariable = currentVariableMap.get(key);
|
||||
|
||||
if (existingVariable) {
|
||||
if (existingVariable.value !== value) {
|
||||
await updateGitLabVariable({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
key,
|
||||
variable: {
|
||||
value,
|
||||
variable_type: existingVariable.variable_type,
|
||||
environment_scope: targetEnvironment || existingVariable.environment_scope,
|
||||
protected: destinationConfig.shouldProtectSecrets ?? existingVariable.protected,
|
||||
...(!existingVariable.masked && destinationConfig.shouldMaskSecrets && { masked: value?.length > 8 }),
|
||||
...(!existingVariable.hidden &&
|
||||
destinationConfig.shouldHideSecrets && { masked_and_hidden: value?.length > 8 }),
|
||||
description: existingVariable.description ?? undefined
|
||||
},
|
||||
targetEnvironment
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await createGitLabVariable({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
variable: {
|
||||
key,
|
||||
value,
|
||||
variable_type: "env_var",
|
||||
environment_scope: targetEnvironment || "*",
|
||||
protected: destinationConfig.shouldProtectSecrets || false,
|
||||
masked: value?.length > 8 ? destinationConfig.shouldMaskSecrets || false : false,
|
||||
masked_and_hidden: value?.length > 8 ? destinationConfig.shouldHideSecrets || false : false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!secretSync.syncOptions.disableSecretDeletion) {
|
||||
for (const variable of currentVariables) {
|
||||
const shouldDelete =
|
||||
matchesSchema(variable.key, environment?.slug || "", secretSync.syncOptions.keySchema) &&
|
||||
!(variable.key in secretMap);
|
||||
|
||||
if (shouldDelete) {
|
||||
await deleteGitLabVariable({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
key: variable.key,
|
||||
targetEnvironment
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: "batch_sync"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeSecrets: async (
|
||||
secretSync: TGitLabSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
{ appConnectionDAL, kmsService }: TGitLabSyncFactoryDeps
|
||||
): Promise<void> => {
|
||||
const { connection, destinationConfig } = secretSync;
|
||||
|
||||
const { projectId, targetEnvironment } = destinationConfig;
|
||||
|
||||
const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService);
|
||||
|
||||
try {
|
||||
for (const key of Object.keys(secretMap)) {
|
||||
await deleteGitLabVariable({
|
||||
accessToken,
|
||||
connection,
|
||||
projectId,
|
||||
key,
|
||||
targetEnvironment
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: "batch_remove"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getSecrets: async (secretSync: TGitLabSyncWithCredentials): Promise<TSecretMap> => {
|
||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
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.Individual).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),
|
||||
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)
|
||||
})
|
||||
]);
|
||||
|
||||
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)
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
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<typeof GitLabSyncSchema>;
|
||||
export type TGitLabSyncInput = z.infer<typeof CreateGitLabSyncSchema>;
|
||||
export type TGitLabSyncListItem = z.infer<typeof GitLabSyncListItemSchema>;
|
||||
|
||||
export type TGitLabSyncWithCredentials = TGitLabSync & {
|
||||
connection: TGitLabConnection;
|
||||
};
|
||||
|
||||
// GitLab CI/CD Variable structure based on API documentation
|
||||
export type TGitLabVariable = {
|
||||
key: string;
|
||||
value: string;
|
||||
variable_type: "env_var" | "file";
|
||||
protected: boolean;
|
||||
masked: boolean;
|
||||
hidden: boolean;
|
||||
raw: boolean;
|
||||
environment_scope: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
// Type for creating a new variable
|
||||
export type TGitLabVariableCreate = {
|
||||
key: string;
|
||||
value: string;
|
||||
variable_type?: "env_var" | "file";
|
||||
protected?: boolean;
|
||||
masked?: boolean;
|
||||
raw?: boolean;
|
||||
environment_scope?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// Type for updating an existing variable
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./gitlab-sync-constants";
|
||||
export * from "./gitlab-sync-fns";
|
||||
export * from "./gitlab-sync-schemas";
|
||||
export * from "./gitlab-sync-types";
|
||||
@@ -18,7 +18,8 @@ export enum SecretSync {
|
||||
OnePass = "1password",
|
||||
Heroku = "heroku",
|
||||
Render = "render",
|
||||
Flyio = "flyio"
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||
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";
|
||||
@@ -63,7 +64,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION,
|
||||
[SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION,
|
||||
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
|
||||
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION
|
||||
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
|
||||
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -227,6 +229,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 });
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -313,6 +317,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.Flyio:
|
||||
secretMap = await FlyioSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.GitLab:
|
||||
secretMap = await GitLabSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -386,6 +393,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 });
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
@@ -21,7 +21,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.OnePass]: "1Password",
|
||||
[SecretSync.Heroku]: "Heroku",
|
||||
[SecretSync.Render]: "Render",
|
||||
[SecretSync.Flyio]: "Fly.io"
|
||||
[SecretSync.Flyio]: "Fly.io",
|
||||
[SecretSync.GitLab]: "GitLab"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -44,7 +45,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.OnePass]: AppConnection.OnePass,
|
||||
[SecretSync.Heroku]: AppConnection.Heroku,
|
||||
[SecretSync.Render]: AppConnection.Render,
|
||||
[SecretSync.Flyio]: AppConnection.Flyio
|
||||
[SecretSync.Flyio]: AppConnection.Flyio,
|
||||
[SecretSync.GitLab]: AppConnection.GitLab
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
@@ -67,5 +69,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.OnePass]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Heroku]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Render]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Flyio]: SecretSyncPlanType.Regular
|
||||
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.GitLab]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
} from "./azure-key-vault";
|
||||
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,
|
||||
@@ -127,7 +128,8 @@ export type TSecretSync =
|
||||
| TOnePassSync
|
||||
| THerokuSync
|
||||
| TRenderSync
|
||||
| TFlyioSync;
|
||||
| TFlyioSync
|
||||
| TGitLabSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -149,7 +151,8 @@ export type TSecretSyncWithCredentials =
|
||||
| TOnePassSyncWithCredentials
|
||||
| THerokuSyncWithCredentials
|
||||
| TRenderSyncWithCredentials
|
||||
| TFlyioSyncWithCredentials;
|
||||
| TFlyioSyncWithCredentials
|
||||
| TGitLabSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -171,7 +174,8 @@ export type TSecretSyncInput =
|
||||
| TOnePassSyncInput
|
||||
| THerokuSyncInput
|
||||
| TRenderSyncInput
|
||||
| TFlyioSyncInput;
|
||||
| TFlyioSyncInput
|
||||
| TGitLabSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -193,7 +197,8 @@ export type TSecretSyncListItem =
|
||||
| TOnePassSyncListItem
|
||||
| THerokuSyncListItem
|
||||
| TRenderSyncListItem
|
||||
| TFlyioSyncListItem;
|
||||
| TFlyioSyncListItem
|
||||
| TGitLabSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
Reference in New Issue
Block a user