mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 18:19:50 +00:00
feat(secret-sync): Add gitlab secret sync
This commit is contained in:
@@ -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