mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 12:04:03 +00:00
feat(secret-sync): Add gitlab secret sync
This commit is contained in:
@@ -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";
|
||||
Reference in New Issue
Block a user