Merge pull request #944 from Infisical/gcp-sm-integration-auth

Add support for service account JSON authentication method for GCP Secret Manager integration
This commit is contained in:
BlackMagiq
2023-09-04 14:20:15 +01:00
committed by GitHub
37 changed files with 742 additions and 277 deletions
@@ -14,8 +14,10 @@ import {
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";
/*** /***
* Return integration authorization with id [integrationAuthId] * Return integration authorization with id [integrationAuthId]
@@ -88,7 +90,7 @@ export const oAuthExchange = async (req: Request, res: Response) => {
* @param req * @param req
* @param res * @param res
*/ */
export const saveIntegrationAccessToken = async (req: Request, res: Response) => { export const saveIntegrationToken = async (req: Request, res: Response) => {
// TODO: refactor // TODO: refactor
// TODO: check if access token is valid for each integration // TODO: check if access token is valid for each integration
@@ -96,14 +98,16 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
const { const {
workspaceId, workspaceId,
accessId, accessId,
refreshToken,
accessToken, accessToken,
url, url,
namespace, namespace,
integration integration
}: { }: {
workspaceId: string; workspaceId: string;
accessId: string | null; accessId: string | undefined;
accessToken: string; refreshToken: string | undefined;
accessToken: string | undefined;
url: string; url: string;
namespace: string; namespace: string;
integration: string; integration: string;
@@ -127,21 +131,36 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
url, url,
namespace, namespace,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8,
...(integration === INTEGRATION_GCP_SECRET_MANAGER ? {
metadata: {
authMethod: "serviceAccount"
}
} : {})
}, },
{ {
new: true, new: true,
upsert: true upsert: true
} }
); );
// encrypt and save integration access details
if (refreshToken) {
await exchangeRefresh({
integrationAuth,
refreshToken
});
}
// encrypt and save integration access details // encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({ if (accessId || accessToken) {
integrationAuthId: integrationAuth._id.toString(), integrationAuth = await IntegrationService.setIntegrationAuthAccess({
accessId, integrationAuthId: integrationAuth._id.toString(),
accessToken, accessId,
accessExpiresAt: undefined accessToken,
}); accessExpiresAt: undefined
});
}
if (!integrationAuth) throw new Error("Failed to save integration access token"); if (!integrationAuth) throw new Error("Failed to save integration access token");
+45 -29
View File
@@ -1,20 +1,23 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Bot, IntegrationAuth } from "../models"; import { Bot, IIntegrationAuth, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh } from "../integrations"; import { exchangeCode, exchangeRefresh } from "../integrations";
import { BotService } from "../services"; import { BotService } from "../services";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_VERCEL INTEGRATION_VERCEL,
INTEGRATION_GCP_SECRET_MANAGER,
} from "../variables"; } from "../variables";
import { UnauthorizedRequestError } from "../utils/errors"; import { BadRequestError, InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { IntegrationAuthMetadata } from "../models/integrationAuth/types";
interface Update { interface Update {
workspace: string; workspace: string;
integration: string; integration: string;
teamId?: string; teamId?: string;
accountId?: string; accountId?: string;
metadata?: IntegrationAuthMetadata
} }
/** /**
@@ -64,6 +67,10 @@ export const handleOAuthExchangeHelper = async ({
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
update.accountId = res.accountId; update.accountId = res.accountId;
case INTEGRATION_GCP_SECRET_MANAGER:
update.metadata = {
authMethod: "oauth2"
}
break; break;
} }
@@ -93,7 +100,6 @@ export const handleOAuthExchangeHelper = async ({
// set integration auth access token // set integration auth access token
await setIntegrationAuthAccessHelper({ await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken, accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt accessExpiresAt: res.accessExpiresAt
}); });
@@ -158,22 +164,24 @@ export const getIntegrationAuthAccessHelper = async ({
message: "Failed to locate Integration Authentication credentials" message: "Failed to locate Integration Authentication credentials"
}); });
accessToken = await BotService.decryptSymmetric({ if (integrationAuth.accessCiphertext && integrationAuth.accessIV && integrationAuth.accessTag) {
workspaceId: integrationAuth.workspace, accessToken = await BotService.decryptSymmetric({
ciphertext: integrationAuth.accessCiphertext as string, workspaceId: integrationAuth.workspace,
iv: integrationAuth.accessIV as string, ciphertext: integrationAuth.accessCiphertext as string,
tag: integrationAuth.accessTag as string iv: integrationAuth.accessIV as string,
}); tag: integrationAuth.accessTag as string
});
}
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { if (integrationAuth?.refreshCiphertext) {
// there is a access token expiration date // there is a access token expiration date
// and refresh token to exchange with the OAuth2 server // and refresh token to exchange with the OAuth2 server
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId
});
if (integrationAuth.accessExpiresAt < new Date()) { if (integrationAuth?.accessExpiresAt && integrationAuth.accessExpiresAt < new Date()) {
// access token is expired // access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId
});
accessToken = await exchangeRefresh({ accessToken = await exchangeRefresh({
integrationAuth, integrationAuth,
refreshToken refreshToken
@@ -194,6 +202,8 @@ export const getIntegrationAuthAccessHelper = async ({
}); });
} }
if (!accessToken) throw InternalServerError();
return { return {
accessId, accessId,
accessToken accessToken
@@ -214,7 +224,7 @@ export const setIntegrationAuthRefreshHelper = async ({
}: { }: {
integrationAuthId: string; integrationAuthId: string;
refreshToken: string; refreshToken: string;
}) => { }): Promise<IIntegrationAuth> => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId); let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth"); if (!integrationAuth) throw new Error("Failed to find integration auth");
@@ -239,6 +249,8 @@ export const setIntegrationAuthRefreshHelper = async ({
new: true new: true
} }
); );
if (!integrationAuth) throw InternalServerError();
return integrationAuth; return integrationAuth;
}; };
@@ -259,20 +271,24 @@ export const setIntegrationAuthAccessHelper = async ({
accessExpiresAt accessExpiresAt
}: { }: {
integrationAuthId: string; integrationAuthId: string;
accessId: string | null; accessId?: string;
accessToken: string; accessToken?: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) => { }) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId); let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth"); if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({ let encryptedAccessTokenObj;
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
let encryptedAccessIdObj; let encryptedAccessIdObj;
if (accessToken) {
encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
}
if (accessId) { if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({ encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
@@ -286,11 +302,11 @@ export const setIntegrationAuthAccessHelper = async ({
}, },
{ {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined, accessIdIV: encryptedAccessIdObj?.iv,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined, accessIdTag: encryptedAccessIdObj?.tag,
accessCiphertext: encryptedAccessTokenObj.ciphertext, accessCiphertext: encryptedAccessTokenObj?.ciphertext,
accessIV: encryptedAccessTokenObj.iv, accessIV: encryptedAccessTokenObj?.iv,
accessTag: encryptedAccessTokenObj.tag, accessTag: encryptedAccessTokenObj?.tag,
accessExpiresAt, accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
+9 -1
View File
@@ -46,6 +46,14 @@ interface ExchangeCodeAzureResponse {
id_token: string; id_token: string;
} }
interface ExchangeCodeGCPResponse {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
}
interface ExchangeCodeHerokuResponse { interface ExchangeCodeHerokuResponse {
token_type: string; token_type: string;
access_token: string; access_token: string;
@@ -174,7 +182,7 @@ const exchangeCode = async ({
const exchangeCodeGCP = async ({ code }: { code: string }) => { const exchangeCodeGCP = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
const res: ExchangeCodeAzureResponse = ( const res: ExchangeCodeGCPResponse = (
await standardRequest.post( await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL, INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
+101 -5
View File
@@ -1,3 +1,4 @@
import jwt from "jsonwebtoken";
import { standardRequest } from "../config/request"; import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models"; import { IIntegrationAuth } from "../models";
import { import {
@@ -6,6 +7,9 @@ import {
INTEGRATION_BITBUCKET_TOKEN_URL, INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE
} from "../variables"; } from "../variables";
import { import {
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
@@ -21,6 +25,8 @@ import {
getClientSecretBitBucket, getClientSecretBitBucket,
getClientSecretGitLab, getClientSecretGitLab,
getClientSecretHeroku, getClientSecretHeroku,
getClientIdGCPSecretManager,
getClientSecretGCPSecretManager,
getSiteURL, getSiteURL,
} from "../config"; } from "../config";
@@ -59,6 +65,19 @@ interface RefreshTokenBitBucketResponse {
state: string; state: string;
} }
interface ServiceAccountAccessTokenGCPSecretManagerResponse {
access_token: string;
expires_in: number;
token_type: string;
}
interface RefreshTokenGCPSecretManagerResponse {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
}
/** /**
* Return new access token by exchanging refresh token [refreshToken] for integration * Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration] * named [integration]
@@ -101,18 +120,23 @@ const exchangeRefresh = async ({
refreshToken, refreshToken,
}); });
break; break;
case INTEGRATION_GCP_SECRET_MANAGER:
tokenDetails = await exchangeRefreshGCPSecretManager({
integrationAuth,
refreshToken,
});
break;
default: default:
throw new Error("Failed to exchange token for incompatible integration"); throw new Error("Failed to exchange token for incompatible integration");
} }
if ( if (
tokenDetails?.accessToken && tokenDetails.accessToken &&
tokenDetails?.refreshToken && tokenDetails.refreshToken &&
tokenDetails?.accessExpiresAt tokenDetails.accessExpiresAt
) { ) {
await IntegrationService.setIntegrationAuthAccess({ await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken, accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt, accessExpiresAt: tokenDetails.accessExpiresAt,
}); });
@@ -278,4 +302,76 @@ const exchangeRefreshBitBucket = async ({
}; };
}; };
export { exchangeRefresh }; /**
* Return new access token by exchanging refresh token [refreshToken] for the
* GCP Secret Manager integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for GCP Secret Manager
* @returns
*/
const exchangeRefreshGCPSecretManager = async ({
integrationAuth,
refreshToken,
}: {
integrationAuth: IIntegrationAuth;
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
if (integrationAuth.metadata?.authMethod === "serviceAccount") {
const serviceAccount = JSON.parse(refreshToken);
const payload = {
iss: serviceAccount.client_email,
aud: serviceAccount.token_uri,
scope: INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
};
const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: 'RS256' });
const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } = await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: token
}).toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken,
accessExpiresAt
};
}
const { data }: { data: RefreshTokenGCPSecretManagerResponse } = (
await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({
client_id: await getClientIdGCPSecretManager(),
client_secret: await getClientSecretGCPSecretManager(),
refresh_token: refreshToken,
grant_type: "refresh_token",
} as any)
)
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken,
accessExpiresAt,
};
};
export { exchangeRefresh };
+3
View File
@@ -1,3 +1,4 @@
import jwt from "jsonwebtoken";
import { import {
CreateSecretCommand, CreateSecretCommand,
GetSecretValueCommand, GetSecretValueCommand,
@@ -28,6 +29,8 @@ 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,
-199
View File
@@ -1,199 +0,0 @@
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL
} from "../variables";
import { Document, Schema, Types, model } from "mongoose";
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "cloudflare-pages"
| "codefresh"
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66"
| "terraform-cloud"
| "teamcity"
| "northflank"
| "windmill"
| "gcp-secret-manager";
teamId: string;
accountId: string;
url: string;
namespace: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessIdCiphertext?: string;
accessIdIV?: string;
accessIdTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
algorithm?: "aes-256-gcm";
keyEncoding?: "utf8" | "base64";
accessExpiresAt?: Date;
}
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_WINDMILL,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK,
INTEGRATION_GCP_SECRET_MANAGER
],
required: true,
},
teamId: {
// vercel-specific integration param
type: String,
},
url: {
// for any self-hosted integrations (e.g. self-hosted hashicorp-vault)
type: String,
},
namespace: {
// hashicorp-vault-specific integration param
type: String,
},
accountId: {
// netlify-specific integration param
type: String,
},
refreshCiphertext: {
type: String,
select: false,
},
refreshIV: {
type: String,
select: false,
},
refreshTag: {
type: String,
select: false,
},
accessIdCiphertext: {
type: String,
select: false,
},
accessIdIV: {
type: String,
select: false,
},
accessIdTag: {
type: String,
select: false,
},
accessCiphertext: {
type: String,
select: false,
},
accessIV: {
type: String,
select: false,
},
accessTag: {
type: String,
select: false,
},
accessExpiresAt: {
type: Date,
select: false,
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true,
},
},
{
timestamps: true,
}
);
export const IntegrationAuth = model<IIntegrationAuth>(
"IntegrationAuth",
integrationAuthSchema
);
@@ -0,0 +1 @@
export * from "./integrationAuth";
@@ -0,0 +1,204 @@
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL
} from "../../variables";
import { Document, Schema, Types, model } from "mongoose";
import { IntegrationAuthMetadata } from "./types";
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "cloudflare-pages"
| "codefresh"
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66"
| "terraform-cloud"
| "teamcity"
| "northflank"
| "windmill"
| "gcp-secret-manager";
teamId: string;
accountId: string;
url: string;
namespace: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessIdCiphertext?: string;
accessIdIV?: string;
accessIdTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
algorithm?: "aes-256-gcm";
keyEncoding?: "utf8" | "base64";
accessExpiresAt?: Date;
metadata?: IntegrationAuthMetadata;
}
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_WINDMILL,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK,
INTEGRATION_GCP_SECRET_MANAGER
],
required: true,
},
teamId: {
// vercel-specific integration param
type: String,
},
url: {
// for any self-hosted integrations (e.g. self-hosted hashicorp-vault)
type: String,
},
namespace: {
// hashicorp-vault-specific integration param
type: String,
},
accountId: {
// netlify-specific integration param
type: String,
},
refreshCiphertext: {
type: String,
select: false,
},
refreshIV: {
type: String,
select: false,
},
refreshTag: {
type: String,
select: false,
},
accessIdCiphertext: {
type: String,
select: false,
},
accessIdIV: {
type: String,
select: false,
},
accessIdTag: {
type: String,
select: false,
},
accessCiphertext: {
type: String,
select: false,
},
accessIV: {
type: String,
select: false,
},
accessTag: {
type: String,
select: false,
},
accessExpiresAt: {
type: Date,
select: false,
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true,
},
metadata: {
type: Schema.Types.Mixed
}
},
{
timestamps: true,
}
);
export const IntegrationAuth = model<IIntegrationAuth>(
"IntegrationAuth",
integrationAuthSchema
);
@@ -0,0 +1,5 @@
interface GCPIntegrationAuthMetadata {
authMethod: "oauth2" | "serviceAccount"
}
export type IntegrationAuthMetadata = GCPIntegrationAuthMetadata;
@@ -22,7 +22,6 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
} }
: {}), : {}),
isActive: true, isActive: true,
app: { $ne: null }
}); });
// for each workspace integration, sync/push secrets // for each workspace integration, sync/push secrets
+2 -2
View File
@@ -25,9 +25,9 @@ router.post(
location: "body", location: "body",
}), }),
body("integrationAuthId").exists().isString().trim(), body("integrationAuthId").exists().isString().trim(),
body("app").trim(),
body("isActive").exists().isBoolean(), body("isActive").exists().isBoolean(),
body("appId").trim(), body("app").optional().isString().trim(),
body("appId").optional().isString().trim(),
body("secretPath").default("/").isString().trim(), body("secretPath").default("/").isString().trim(),
body("sourceEnvironment").trim(), body("sourceEnvironment").trim(),
body("targetEnvironment").trim(), body("targetEnvironment").trim(),
+5 -3
View File
@@ -29,6 +29,7 @@ router.get(
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
attachAccessToken: false
}), }),
param("integrationAuthId"), param("integrationAuthId"),
validateRequest, validateRequest,
@@ -54,8 +55,9 @@ router.post(
router.post( router.post(
"/access-token", "/access-token",
body("workspaceId").exists().trim().notEmpty(), body("workspaceId").exists().trim().notEmpty(),
body("accessId").trim(), body("refreshToken").optional().isString().trim().notEmpty(),
body("accessToken").exists().trim().notEmpty(), body("accessId").optional().isString().trim(),
body("accessToken").optional().isString().trim().notEmpty(),
body("url").trim(), body("url").trim(),
body("namespace").trim(), body("namespace").trim(),
body("integration").exists().trim().notEmpty(), body("integration").exists().trim().notEmpty(),
@@ -67,7 +69,7 @@ router.post(
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body", locationWorkspaceId: "body",
}), }),
integrationAuthController.saveIntegrationAccessToken integrationAuthController.saveIntegrationToken
); );
router.get( router.get(
+4 -3
View File
@@ -7,6 +7,7 @@ import {
setIntegrationAuthRefreshHelper, setIntegrationAuthRefreshHelper,
} from "../helpers/integration"; } from "../helpers/integration";
import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices"; import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices";
import { IIntegrationAuth } from "../models";
/** /**
* Class to handle integrations * Class to handle integrations
@@ -102,7 +103,7 @@ class IntegrationService {
}: { }: {
integrationAuthId: string; integrationAuthId: string;
refreshToken: string; refreshToken: string;
}) { }): Promise<IIntegrationAuth> {
return await setIntegrationAuthRefreshHelper({ return await setIntegrationAuthRefreshHelper({
integrationAuthId, integrationAuthId,
refreshToken, refreshToken,
@@ -127,8 +128,8 @@ class IntegrationService {
accessExpiresAt, accessExpiresAt,
}: { }: {
integrationAuthId: string; integrationAuthId: string;
accessId: string | null; accessId?: string;
accessToken: string; accessToken?: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) { }) {
return await setIntegrationAuthAccessHelper({ return await setIntegrationAuthAccessHelper({
+2 -1
View File
@@ -68,7 +68,7 @@ export const INTEGRATION_SET = new Set([
export const INTEGRATION_OAUTH2 = "oauth2"; export const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints // integration oauth endpoints
export const INTEGRATION_GCP_TOKEN_URL = "https://accounts.google.com/o/oauth2/token"; export const INTEGRATION_GCP_TOKEN_URL = "https://oauth2.googleapis.com/token";
export const INTEGRATION_AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; export const INTEGRATION_AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
export const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token"; export const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token";
export const INTEGRATION_VERCEL_TOKEN_URL = export const INTEGRATION_VERCEL_TOKEN_URL =
@@ -105,6 +105,7 @@ export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com";
export const INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com" export const INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com"
export const INTEGRATION_GCP_SECRET_MANAGER_URL = `https://${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`; export const INTEGRATION_GCP_SECRET_MANAGER_URL = `https://${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`;
export const INTEGRATION_GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com"; export const INTEGRATION_GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com";
export const INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
export const getIntegrationOptions = async () => { export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

@@ -16,7 +16,7 @@ Navigate to your IAM user permissions and add a permission policy to grant acces
![integration IAM 2](../../images/integrations-aws-parameter-store-iam-2.png) ![integration IAM 2](../../images/integrations-aws-parameter-store-iam-2.png)
![integrations IAM 3](../../images/integrations-aws-parameter-store-iam-3.png) ![integrations IAM 3](../../images/integrations-aws-parameter-store-iam-3.png)
For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Parameter Store for the IAM user that you can use: For enhanced security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Parameter Store for the IAM user that you can use:
```json ```json
{ {
+63 -4
View File
@@ -5,17 +5,24 @@ description: "How to sync secrets from Infisical to GCP Secret Manager"
<Tabs> <Tabs>
<Tab title="Usage"> <Tab title="Usage">
<AccordionGroup>
<Accordion title="Connect with OAuth2">
Prerequisites: Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png) ![integrations](../../images/integrations.png)
## Authorize Infisical for GCP ## Authorize Infisical for GCP
Press on the GCP Secret Manager tile and grant Infisical access to GCP. Press on the GCP Secret Manager tile and select **Continue with OAuth**
![integrations GCP authorization options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth-options.png)
Grant Infisical access to GCP.
![integrations GCP authorization](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth.png) ![integrations GCP authorization](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth.png)
@@ -37,9 +44,61 @@ Select which Infisical environment secrets you want to sync to which GCP secret
Using Infisical to sync secrets to GCP Secret Manager requires that you enable Using Infisical to sync secrets to GCP Secret Manager requires that you enable
the Service Usage API in the Google Cloud project you want to sync secrets to. More on that [here](https://cloud.google.com/service-usage/docs/set-up-development-environment). the Service Usage API in the Google Cloud project you want to sync secrets to. More on that [here](https://cloud.google.com/service-usage/docs/set-up-development-environment).
</Warning> </Warning>
</Accordion>
<Accordion title="Connect with Service Account JSON">
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Have a GCP project and have/create a [service account](https://cloud.google.com/iam/docs/service-account-overview) in it
## Grant the service account permissions for GCP Secret Manager
Navigate to **IAM & Admin** page in GCP and add the **Secret Manager Admin** and **Service Usage Admin** roles to the service account.
![integrations GCP secret manager IAM](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-iam.png)
<Info>
For enhanced security, you may want to assign more granular permissions to the service account. At minimum,
the service account should be able to read/write secrets from/to GCP Secret Manager (e.g. **Secret Manager Admin** role)
and list which GCP services are enabled/disabled (e.g. **Service Usage Admin** role).
</Info>
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for GCP
Press on the GCP Secret Manager tile and paste in your **GCP Service Account JSON** (you can create and download the JSON for your
service account in IAM & Admin > Service Accounts > Service Account > Keys).
![integrations GCP authorization IAM key](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-iam-key.png)
![integrations GCP authorization options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth-options.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to the GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager.
![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png)
![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png)
<Warning>
Using Infisical to sync secrets to GCP Secret Manager requires that you enable
the Service Usage API in the Google Cloud project you want to sync secrets to. More on that [here](https://cloud.google.com/service-usage/docs/set-up-development-environment).
</Warning>
</Accordion>
</AccordionGroup>
</Tab> </Tab>
<Tab title="Self-Hosted Setup"> <Tab title="Self-Hosted Setup">
Using the GCP Secret Manager integration on a self-hosted instance of Infisical requires configuring an OAuth2 application in GCP Using the GCP Secret Manager integration (via the OAuth2 method) on a self-hosted instance of Infisical requires configuring an OAuth2 application in GCP
and registering your instance with it. and registering your instance with it.
## Create an OAuth2 application in GCP ## Create an OAuth2 application in GCP
@@ -394,6 +394,7 @@ export const useSaveIntegrationAccessToken = () => {
mutationFn: async ({ mutationFn: async ({
workspaceId, workspaceId,
integration, integration,
refreshToken,
accessId, accessId,
accessToken, accessToken,
url, url,
@@ -401,14 +402,16 @@ export const useSaveIntegrationAccessToken = () => {
}: { }: {
workspaceId: string | null; workspaceId: string | null;
integration: string | undefined; integration: string | undefined;
accessId: string | null; refreshToken?: string;
accessToken: string; accessId?: string;
accessToken?: string;
url: string | null; url: string | null;
namespace: string | null; namespace: string | null;
}) => { }) => {
const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/access-token", { const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/access-token", {
workspaceId, workspaceId,
integration, integration,
refreshToken,
accessId, accessId,
accessToken, accessToken,
url, url,
@@ -46,8 +46,8 @@ export const useCreateIntegration = () => {
integrationAuthId: string; integrationAuthId: string;
isActive: boolean; isActive: boolean;
secretPath: string; secretPath: string;
app: string | null; app?: string | null;
appId: string | null; appId?: string | null;
sourceEnvironment: string; sourceEnvironment: string;
targetEnvironment: string | null; targetEnvironment: string | null;
targetEnvironmentId: string | null; targetEnvironmentId: string | null;
@@ -28,7 +28,6 @@ export default function ChecklyCreateIntegrationPage() {
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "checkly", integration: "checkly",
accessId: null,
accessToken, accessToken,
url: null, url: null,
namespace: null namespace: null
@@ -29,7 +29,6 @@ export default function CircleCICreateIntegrationPage() {
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "circleci", integration: "circleci",
accessToken: apiKey, accessToken: apiKey,
accessId: null,
url: null, url: null,
namespace:null namespace:null
}); });
@@ -28,7 +28,6 @@ export default function Cloud66CreateIntegrationPage() {
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "cloud-66", integration: "cloud-66",
accessId: null,
accessToken: apiKey, accessToken: apiKey,
url: null, url: null,
namespace: null namespace: null
@@ -28,7 +28,6 @@ export default function CodefreshCreateIntegrationPage() {
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "codefresh", integration: "codefresh",
accessId: null,
accessToken: apiKey, accessToken: apiKey,
url: null, url: null,
namespace: null namespace: null
@@ -28,7 +28,6 @@ export default function DigitalOceanAppPlatformCreateIntegrationPage() {
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "digital-ocean-app-platform", integration: "digital-ocean-app-platform",
accessId: null,
accessToken: apiKey, accessToken: apiKey,
url: null, url: null,
namespace: null namespace: null
@@ -28,7 +28,6 @@ export default function FlyioCreateIntegrationPage() {
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "flyio", integration: "flyio",
accessId: null,
accessToken, accessToken,
url: null, url: null,
namespace: null namespace: null
@@ -0,0 +1,107 @@
import crypto from "crypto";
import { useState } from "react";
import { useRouter } from "next/router";
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
useGetCloudIntegrations,
useSaveIntegrationAccessToken} from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, TextArea } from "../../../components/v2";
export default function GCPSecretManagerAuthorizeIntegrationPage() {
const router = useRouter();
const { data: cloudIntegrations } = useGetCloudIntegrations();
const { mutateAsync } = useSaveIntegrationAccessToken();
const [accessToken, setAccessToken] = useState("");
const [accessTokenErrorText, setAccessTokenErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleIntegrateWithPAT = async () => {
try {
setAccessTokenErrorText("");
if (accessToken.length === 0) {
setAccessTokenErrorText("Service account JSON cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"),
integration: "gcp-secret-manager",
refreshToken: accessToken,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/gcp-secret-manager/pat/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
const handleIntegrateWithOAuth = () => {
if (!cloudIntegrations) return;
const integrationOption = cloudIntegrations.find((integration) => integration.slug === "gcp-secret-manager");
if (!integrationOption) return;
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
const link = `https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/cloud-platform&response_type=code&access_type=offline&state=${state}&redirect_uri=${window.location.origin}/integrations/gcp-secret-manager/oauth2/callback&client_id=${integrationOption.clientId}`;
window.location.assign(link);
}
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">GCP Secret Manager Integration</CardTitle>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={handleIntegrateWithOAuth}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-11 w-full mx-0 mt-4"
>
Continue with OAuth
</Button>
<div className='w-full flex flex-row items-center my-4 py-2'>
<div className='w-full border-t border-mineshaft-400/60' />
<span className="mx-2 text-mineshaft-200 text-xs">or</span>
<div className='w-full border-t border-mineshaft-400/60' />
</div>
<FormControl
label="GCP Service Account JSON"
errorText={accessTokenErrorText}
isError={accessTokenErrorText !== "" ?? false}
>
<TextArea
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
placeholder=""
className="h-48"
/>
</FormControl>
<Button
onClick={handleIntegrateWithPAT}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to GCP Secret Manager
</Button>
</Card>
</div>
);
}
GCPSecretManagerAuthorizeIntegrationPage.requireAuth = true;
@@ -0,0 +1,153 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import {
useCreateIntegration
} from "@app/hooks/api";
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
export default function GCPSecretManagerCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(integrationAuthApps[0].appId as string);
} else {
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await mutateAsync({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name ?? null,
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps
? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">GCP Secret Manager Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GCP Project">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
// isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
GCPSecretManagerCreateIntegrationPage.requireAuth = true;
@@ -29,7 +29,6 @@ export default function NorthflankCreateIntegrationPage() {
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "northflank", integration: "northflank",
accessToken: apiKey, accessToken: apiKey,
accessId: null,
url: null, url: null,
namespace: null namespace: null
}); });
@@ -28,7 +28,6 @@ export default function RailwayAuthorizeIntegrationPage() {
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "railway", integration: "railway",
accessId: null,
accessToken: apiKey, accessToken: apiKey,
url: null, url: null,
namespace: null namespace: null
@@ -26,7 +26,6 @@ export default function RenderCreateIntegrationPage() {
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "render", integration: "render",
accessId: null,
accessToken: apiKey, accessToken: apiKey,
url: null, url: null,
namespace: null namespace: null
@@ -29,7 +29,6 @@ export default function SupabaseCreateIntegrationPage() {
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "supabase", integration: "supabase",
accessToken: apiKey, accessToken: apiKey,
accessId: null,
url: null, url: null,
namespace: null namespace: null
}); });
@@ -37,7 +37,6 @@ export default function TeamCityCreateIntegrationPage() {
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "teamcity", integration: "teamcity",
accessId: null,
accessToken: apiKey, accessToken: apiKey,
url: serverUrl, url: serverUrl,
namespace: null namespace: null
@@ -29,7 +29,6 @@ export default function TravisCICreateIntegrationPage() {
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "travisci", integration: "travisci",
accessToken: apiKey, accessToken: apiKey,
accessId: null,
url: null, url: null,
namespace: null namespace: null
}); });
@@ -30,7 +30,6 @@ export default function WindmillCreateIntegrationPage() {
workspaceId: localStorage.getItem("projectData.id"), workspaceId: localStorage.getItem("projectData.id"),
integration: "windmill", integration: "windmill",
accessToken: apiKey, accessToken: apiKey,
accessId: null,
url: null, url: null,
namespace: null namespace: null
}); });
@@ -40,7 +40,7 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
let link = ""; let link = "";
switch (integrationOption.slug) { switch (integrationOption.slug) {
case "gcp-secret-manager": case "gcp-secret-manager":
link = `https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/cloud-platform&response_type=code&access_type=offline&state=${state}&redirect_uri=${window.location.origin}/integrations/gcp-secret-manager/oauth2/callback&client_id=${integrationOption.clientId}`; link = `${window.location.origin}/integrations/gcp-secret-manager/authorize`;
break; break;
case "azure-key-vault": case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`; link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;