Finish adding support for self-hosted GitLab integration

This commit is contained in:
Tuan Dang
2023-09-06 10:57:27 +01:00
parent 04548313ab
commit d07b2dafc3
11 changed files with 73 additions and 39 deletions
@@ -10,11 +10,11 @@ import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
INTEGRATION_BITBUCKET_API_URL, INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_RAILWAY_API_URL, INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_GCP_SECRET_MANAGER,
getIntegrationOptions as getIntegrationOptionsFunc getIntegrationOptions as getIntegrationOptionsFunc
} from "../../variables"; } from "../../variables";
import { exchangeRefresh } from "../../integrations"; import { exchangeRefresh } from "../../integrations";
@@ -51,7 +51,12 @@ export const getIntegrationOptions = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const oAuthExchange = async (req: Request, res: Response) => { export const oAuthExchange = async (req: Request, res: Response) => {
const { workspaceId, code, integration } = req.body; const {
workspaceId,
code,
integration,
url
} = req.body;
if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration"); if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration");
const environments = req.membership.workspace?.environments || []; const environments = req.membership.workspace?.environments || [];
@@ -63,7 +68,8 @@ export const oAuthExchange = async (req: Request, res: Response) => {
workspaceId, workspaceId,
integration, integration,
code, code,
environment: environments[0].slug environment: environments[0].slug,
url
}); });
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
+8 -4
View File
@@ -5,11 +5,11 @@ import { BotService } from "../services";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_GCP_SECRET_MANAGER,
} from "../variables"; } from "../variables";
import { BadRequestError, InternalServerError, UnauthorizedRequestError } from "../utils/errors"; import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { IntegrationAuthMetadata } from "../models/integrationAuth/types"; import { IntegrationAuthMetadata } from "../models/integrationAuth/types";
interface Update { interface Update {
@@ -36,12 +36,14 @@ export const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
integration, integration,
code, code,
environment environment,
url
}: { }: {
workspaceId: string; workspaceId: string;
integration: string; integration: string;
code: string; code: string;
environment: string; environment: string;
url?: string;
}) => { }) => {
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: workspaceId, workspace: workspaceId,
@@ -53,7 +55,8 @@ export const handleOAuthExchangeHelper = async ({
// exchange code for access and refresh tokens // exchange code for access and refresh tokens
const res = await exchangeCode({ const res = await exchangeCode({
integration, integration,
code code,
url
}); });
const update: Update = { const update: Update = {
@@ -67,6 +70,7 @@ export const handleOAuthExchangeHelper = async ({
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
update.accountId = res.accountId; update.accountId = res.accountId;
break;
case INTEGRATION_GCP_SECRET_MANAGER: case INTEGRATION_GCP_SECRET_MANAGER:
update.metadata = { update.metadata = {
authMethod: "oauth2" authMethod: "oauth2"
+11 -2
View File
@@ -118,9 +118,11 @@ interface ExchangeCodeBitBucketResponse {
const exchangeCode = async ({ const exchangeCode = async ({
integration, integration,
code, code,
url
}: { }: {
integration: string; integration: string;
code: string; code: string;
url?: string;
}) => { }) => {
let obj = {} as any; let obj = {} as any;
@@ -158,6 +160,7 @@ const exchangeCode = async ({
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
obj = await exchangeCodeGitlab({ obj = await exchangeCodeGitlab({
code, code,
url
}); });
break; break;
case INTEGRATION_BITBUCKET: case INTEGRATION_BITBUCKET:
@@ -388,11 +391,17 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
* @returns {String} obj2.refreshToken - refresh token for Gitlab API * @returns {String} obj2.refreshToken - refresh token for Gitlab API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeGitlab = async ({ code }: { code: string }) => { const exchangeCodeGitlab = async ({
code,
url
}: {
code: string,
url?: string;
}) => {
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
const res: ExchangeCodeGitlabResponse = ( const res: ExchangeCodeGitlabResponse = (
await standardRequest.post( await standardRequest.post(
INTEGRATION_GITLAB_TOKEN_URL, url ? `${url}/oauth/token` : INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: "authorization_code", grant_type: "authorization_code",
code: code, code: code,
+14 -9
View File
@@ -5,11 +5,11 @@ import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL, INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITLAB, INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE,
INTEGRATION_HEROKU,
INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL, INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE INTEGRATION_GITLAB,
INTEGRATION_HEROKU
} from "../variables"; } from "../variables";
import { import {
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
@@ -20,13 +20,13 @@ import { IntegrationService } from "../services";
import { import {
getClientIdAzure, getClientIdAzure,
getClientIdBitBucket, getClientIdBitBucket,
getClientIdGCPSecretManager,
getClientIdGitLab, getClientIdGitLab,
getClientSecretAzure, getClientSecretAzure,
getClientSecretBitBucket, getClientSecretBitBucket,
getClientSecretGCPSecretManager,
getClientSecretGitLab, getClientSecretGitLab,
getClientSecretHeroku, getClientSecretHeroku,
getClientIdGCPSecretManager,
getClientSecretGCPSecretManager,
getSiteURL, getSiteURL,
} from "../config"; } from "../config";
@@ -112,6 +112,7 @@ const exchangeRefresh = async ({
break; break;
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({ tokenDetails = await exchangeRefreshGitLab({
integrationAuth,
refreshToken, refreshToken,
}); });
break; break;
@@ -226,17 +227,21 @@ const exchangeRefreshHeroku = async ({
* @returns * @returns
*/ */
const exchangeRefreshGitLab = async ({ const exchangeRefreshGitLab = async ({
integrationAuth,
refreshToken, refreshToken,
}: { }: {
integrationAuth: IIntegrationAuth;
refreshToken: string; refreshToken: string;
}) => { }) => {
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
const url = integrationAuth.url;
const { const {
data, data,
}: { }: {
data: RefreshTokenGitLabResponse; data: RefreshTokenGitLabResponse;
} = await standardRequest.post( } = await standardRequest.post(
INTEGRATION_GITLAB_TOKEN_URL, url ? `${url}/oauth/token` : INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: refreshToken, refresh_token: refreshToken,
@@ -329,17 +334,17 @@ const exchangeRefreshGCPSecretManager = async ({
exp: Math.floor(Date.now() / 1000) + 3600, exp: Math.floor(Date.now() / 1000) + 3600,
}; };
const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: 'RS256' }); const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: "RS256" });
const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } = await standardRequest.post( const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } = await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL, INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: token assertion: token
}).toString(), }).toString(),
{ {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' "Content-Type": "application/x-www-form-urlencoded"
} }
} }
); );
-3
View File
@@ -1,4 +1,3 @@
import jwt from "jsonwebtoken";
import { import {
CreateSecretCommand, CreateSecretCommand,
GetSecretValueCommand, GetSecretValueCommand,
@@ -29,8 +28,6 @@ import {
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_SECRET_MANAGER_URL, INTEGRATION_GCP_SECRET_MANAGER_URL,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_GITLAB_API_URL, INTEGRATION_GITLAB_API_URL,
+1
View File
@@ -48,6 +48,7 @@ router.post(
body("workspaceId").exists().trim().notEmpty(), body("workspaceId").exists().trim().notEmpty(),
body("code").exists().trim().notEmpty(), body("code").exists().trim().notEmpty(),
body("integration").exists().trim().notEmpty(), body("integration").exists().trim().notEmpty(),
body("url").optional().isString().trim(),
validateRequest, validateRequest,
integrationAuthController.oAuthExchange integrationAuthController.oAuthExchange
); );
@@ -32,17 +32,20 @@ class IntegrationService {
integration, integration,
code, code,
environment, environment,
url
}: { }: {
workspaceId: string; workspaceId: string;
integration: string; integration: string;
code: string; code: string;
environment: string; environment: string;
url?: string;
}) { }) {
return await handleOAuthExchangeHelper({ return await handleOAuthExchangeHelper({
workspaceId, workspaceId,
integration, integration,
code, code,
environment, environment,
url
}); });
} }
@@ -367,16 +367,19 @@ export const useAuthorizeIntegration = () => {
mutationFn: async ({ mutationFn: async ({
workspaceId, workspaceId,
code, code,
integration integration,
url
}: { }: {
workspaceId: string; workspaceId: string;
code: string; code: string;
integration: string; integration: string;
url?: string;
}) => { }) => {
const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/oauth-token", { const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/oauth-token", {
workspaceId, workspaceId,
code, code,
integration integration,
url
}); });
return integrationAuth; return integrationAuth;
@@ -14,18 +14,20 @@ export default function GitLabAuthorizeIntegrationPage() {
const [gitLabURL, setGitLabURL] = useState(""); const [gitLabURL, setGitLabURL] = useState("");
const handleIntegrateWithOAuth = () => { const handleIntegrateWithOAuth = () => {
if (!cloudIntegrations) return; if (!cloudIntegrations) return;
const integrationOption = cloudIntegrations.find((integration) => integration.slug === "gitlab"); const integrationOption = cloudIntegrations.find((integration) => integration.slug === "gitlab");
if (!integrationOption) return; if (!integrationOption) return;
const baseURL = gitLabURL.trim() === "" ? "https://gitlab.com" : gitLabURL.trim(); const baseURL = gitLabURL.trim() === "" ? "https://gitlab.com" : gitLabURL.trim();
const csrfToken = crypto.randomBytes(16).toString("hex"); const csrfToken = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", csrfToken); localStorage.setItem("latestCSRFToken", csrfToken);
const link = `${baseURL}/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`; const state = `${csrfToken}|${gitLabURL.trim() === "" ? "" : gitLabURL.trim()}`;
window.location.assign(link); const link = `${baseURL}/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
window.location.assign(link);
} }
return ( return (
@@ -95,7 +95,7 @@ export default function GitLabCreateIntegrationPage() {
integrationAuthId: integrationAuth?._id, integrationAuthId: integrationAuth?._id,
isActive: true, isActive: true,
app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name, app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name,
appId: targetAppId, appId: String(targetAppId),
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: targetEnvironment === "" ? "*" : targetEnvironment, targetEnvironment: targetEnvironment === "" ? "*" : targetEnvironment,
targetEnvironmentId: null, targetEnvironmentId: null,
@@ -15,14 +15,18 @@ export default function GitLabOAuth2CallbackPage() {
(async () => { (async () => {
try { try {
// validate state // validate state
if (state !== localStorage.getItem("latestCSRFToken")) return; const [csrfToken, url] = (state as string).split("|", 2);
if (csrfToken !== localStorage.getItem("latestCSRFToken")) return;
localStorage.removeItem("latestCSRFToken"); localStorage.removeItem("latestCSRFToken");
// TODO: self-hosted url somewhere here?
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id") as string, workspaceId: localStorage.getItem("projectData.id") as string,
code: code as string, code: code as string,
integration: "gitlab" integration: "gitlab",
...(url === "" ? {} : {
url
})
}); });
router.push(`/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`); router.push(`/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`);