Finished adding support for service account JSON auth method for GCP secret manager integration

This commit is contained in:
Tuan Dang
2023-09-04 12:48:15 +01:00
parent d9abe671af
commit 4a603da425
24 changed files with 671 additions and 289 deletions

View File

@ -14,8 +14,10 @@ import {
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_GCP_SECRET_MANAGER,
getIntegrationOptions as getIntegrationOptionsFunc
} from "../../variables";
import { exchangeRefresh } from "../../integrations";
/***
* Return integration authorization with id [integrationAuthId]
@ -88,7 +90,7 @@ export const oAuthExchange = async (req: Request, res: Response) => {
* @param req
* @param res
*/
export const saveIntegrationAccessToken = async (req: Request, res: Response) => {
export const saveIntegrationToken = async (req: Request, res: Response) => {
// TODO: refactor
// TODO: check if access token is valid for each integration
@ -96,14 +98,16 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
const {
workspaceId,
accessId,
refreshToken,
accessToken,
url,
namespace,
integration
}: {
workspaceId: string;
accessId: string | null;
accessToken: string;
accessId: string | undefined;
refreshToken: string | undefined;
accessToken: string | undefined;
url: string;
namespace: string;
integration: string;
@ -127,21 +131,36 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
url,
namespace,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
keyEncoding: ENCODING_SCHEME_UTF8,
...(integration === INTEGRATION_GCP_SECRET_MANAGER ? {
metadata: {
authMethod: "serviceAccount"
}
} : {})
},
{
new: true,
upsert: true
}
);
// encrypt and save integration access details
if (refreshToken) {
await exchangeRefresh({
integrationAuth,
refreshToken
});
}
// encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId,
accessToken,
accessExpiresAt: undefined
});
if (accessId || accessToken) {
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId,
accessToken,
accessExpiresAt: undefined
});
}
if (!integrationAuth) throw new Error("Failed to save integration access token");

View File

@ -1,20 +1,23 @@
import { Types } from "mongoose";
import { Bot, IntegrationAuth } from "../models";
import { Bot, IIntegrationAuth, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh } from "../integrations";
import { BotService } from "../services";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
INTEGRATION_NETLIFY,
INTEGRATION_VERCEL
INTEGRATION_VERCEL,
INTEGRATION_GCP_SECRET_MANAGER,
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
import { BadRequestError, InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { IntegrationAuthMetadata } from "../models/integrationAuth/types";
interface Update {
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
metadata?: IntegrationAuthMetadata
}
/**
@ -64,6 +67,10 @@ export const handleOAuthExchangeHelper = async ({
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
case INTEGRATION_GCP_SECRET_MANAGER:
update.metadata = {
authMethod: "oauth2"
}
break;
}
@ -93,7 +100,6 @@ export const handleOAuthExchangeHelper = async ({
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
@ -158,22 +164,24 @@ export const getIntegrationAuthAccessHelper = async ({
message: "Failed to locate Integration Authentication credentials"
});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
});
if (integrationAuth.accessCiphertext && integrationAuth.accessIV && integrationAuth.accessTag) {
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext 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
// 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
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId
});
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken
@ -194,6 +202,8 @@ export const getIntegrationAuthAccessHelper = async ({
});
}
if (!accessToken) throw InternalServerError();
return {
accessId,
accessToken
@ -214,7 +224,7 @@ export const setIntegrationAuthRefreshHelper = async ({
}: {
integrationAuthId: string;
refreshToken: string;
}) => {
}): Promise<IIntegrationAuth> => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth");
@ -239,6 +249,8 @@ export const setIntegrationAuthRefreshHelper = async ({
new: true
}
);
if (!integrationAuth) throw InternalServerError();
return integrationAuth;
};
@ -259,20 +271,24 @@ export const setIntegrationAuthAccessHelper = async ({
accessExpiresAt
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessId?: string;
accessToken?: string;
accessExpiresAt: Date | undefined;
}) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
let encryptedAccessTokenObj;
let encryptedAccessIdObj;
if (accessToken) {
encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
}
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
@ -286,11 +302,11 @@ export const setIntegrationAuthAccessHelper = async ({
},
{
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessIdIV: encryptedAccessIdObj?.iv,
accessIdTag: encryptedAccessIdObj?.tag,
accessCiphertext: encryptedAccessTokenObj?.ciphertext,
accessIV: encryptedAccessTokenObj?.iv,
accessTag: encryptedAccessTokenObj?.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8

View File

@ -46,6 +46,14 @@ interface ExchangeCodeAzureResponse {
id_token: string;
}
interface ExchangeCodeGCPResponse {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
}
interface ExchangeCodeHerokuResponse {
token_type: string;
access_token: string;
@ -174,7 +182,7 @@ const exchangeCode = async ({
const exchangeCodeGCP = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const res: ExchangeCodeAzureResponse = (
const res: ExchangeCodeGCPResponse = (
await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({

View File

@ -1,3 +1,4 @@
import jwt from "jsonwebtoken";
import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models";
import {
@ -6,6 +7,9 @@ import {
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITLAB,
INTEGRATION_HEROKU,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE
} from "../variables";
import {
INTEGRATION_AZURE_TOKEN_URL,
@ -21,6 +25,8 @@ import {
getClientSecretBitBucket,
getClientSecretGitLab,
getClientSecretHeroku,
getClientIdGCPSecretManager,
getClientSecretGCPSecretManager,
getSiteURL,
} from "../config";
@ -59,6 +65,19 @@ interface RefreshTokenBitBucketResponse {
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
* named [integration]
@ -101,18 +120,23 @@ const exchangeRefresh = async ({
refreshToken,
});
break;
case INTEGRATION_GCP_SECRET_MANAGER:
tokenDetails = await exchangeRefreshGCPSecretManager({
integrationAuth,
refreshToken,
});
break;
default:
throw new Error("Failed to exchange token for incompatible integration");
}
if (
tokenDetails?.accessToken &&
tokenDetails?.refreshToken &&
tokenDetails?.accessExpiresAt
tokenDetails.accessToken &&
tokenDetails.refreshToken &&
tokenDetails.accessExpiresAt
) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
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 };

View File

@ -1,3 +1,4 @@
import jwt from "jsonwebtoken";
import {
CreateSecretCommand,
GetSecretValueCommand,
@ -28,6 +29,8 @@ import {
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_SECRET_MANAGER_URL,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_GITLAB_API_URL,

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
);

View File

@ -0,0 +1 @@
export * from "./integrationAuth";

View File

@ -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
);

View File

@ -0,0 +1,5 @@
interface GCPIntegrationAuthMetadata {
authMethod: "oauth2" | "serviceAccount"
}
export type IntegrationAuthMetadata = GCPIntegrationAuthMetadata;

View File

@ -22,7 +22,6 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
}
: {}),
isActive: true,
app: { $ne: null }
});
// for each workspace integration, sync/push secrets

View File

@ -25,9 +25,9 @@ router.post(
location: "body",
}),
body("integrationAuthId").exists().isString().trim(),
body("app").trim(),
body("isActive").exists().isBoolean(),
body("appId").trim(),
body("app").optional().isString().trim(),
body("appId").optional().isString().trim(),
body("secretPath").default("/").isString().trim(),
body("sourceEnvironment").trim(),
body("targetEnvironment").trim(),

View File

@ -29,6 +29,7 @@ router.get(
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
attachAccessToken: false
}),
param("integrationAuthId"),
validateRequest,
@ -54,8 +55,9 @@ router.post(
router.post(
"/access-token",
body("workspaceId").exists().trim().notEmpty(),
body("accessId").trim(),
body("accessToken").exists().trim().notEmpty(),
body("refreshToken").optional().isString().trim().notEmpty(),
body("accessId").optional().isString().trim(),
body("accessToken").optional().isString().trim().notEmpty(),
body("url").trim(),
body("namespace").trim(),
body("integration").exists().trim().notEmpty(),
@ -67,7 +69,7 @@ router.post(
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
}),
integrationAuthController.saveIntegrationAccessToken
integrationAuthController.saveIntegrationToken
);
router.get(

View File

@ -7,6 +7,7 @@ import {
setIntegrationAuthRefreshHelper,
} from "../helpers/integration";
import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices";
import { IIntegrationAuth } from "../models";
/**
* Class to handle integrations
@ -102,7 +103,7 @@ class IntegrationService {
}: {
integrationAuthId: string;
refreshToken: string;
}) {
}): Promise<IIntegrationAuth> {
return await setIntegrationAuthRefreshHelper({
integrationAuthId,
refreshToken,
@ -127,8 +128,8 @@ class IntegrationService {
accessExpiresAt,
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessId?: string;
accessToken?: string;
accessExpiresAt: Date | undefined;
}) {
return await setIntegrationAuthAccessHelper({

View File

@ -68,7 +68,7 @@ export const INTEGRATION_SET = new Set([
export const INTEGRATION_OAUTH2 = "oauth2";
// 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_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token";
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_URL = `https://${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`;
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 () => {
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

View File

@ -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)
![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
{

View File

@ -5,17 +5,24 @@ description: "How to sync secrets from Infisical to GCP Secret Manager"
<Tabs>
<Tab title="Usage">
<AccordionGroup>
<Accordion title="Connect with OAuth2">
Prerequisites:
- 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)
## 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)
@ -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
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>
<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 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.
## Create an OAuth2 application in GCP

View File

@ -394,6 +394,7 @@ export const useSaveIntegrationAccessToken = () => {
mutationFn: async ({
workspaceId,
integration,
refreshToken,
accessId,
accessToken,
url,
@ -401,14 +402,16 @@ export const useSaveIntegrationAccessToken = () => {
}: {
workspaceId: string | null;
integration: string | undefined;
accessId: string | null;
accessToken: string;
refreshToken?: string;
accessId?: string;
accessToken?: string;
url: string | null;
namespace: string | null;
}) => {
const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/access-token", {
workspaceId,
integration,
refreshToken,
accessId,
accessToken,
url,

View File

@ -46,8 +46,8 @@ export const useCreateIntegration = () => {
integrationAuthId: string;
isActive: boolean;
secretPath: string;
app: string | null;
appId: string | null;
app?: string | null;
appId?: string | null;
sourceEnvironment: string;
targetEnvironment: string | null;
targetEnvironmentId: string | null;

View File

@ -1,51 +1,64 @@
import crypto from "crypto";
import { useState } from "react";
import { useRouter } from "next/router";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import {
useSaveIntegrationAccessToken
useSaveIntegrationAccessToken,
useGetCloudIntegrations
} from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, Input, 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 handleButtonClick = async () => {
const handleIntegrateWithPAT = async () => {
try {
setAccessTokenErrorText("");
if (accessToken.length === 0) {
setAccessTokenErrorText("Access token cannot be blank");
setAccessTokenErrorText("Service account JSON cannot be blank");
return;
}
console.log("setAccessTokenErrorText");
console.log("accessToken: ", accessToken);
// setIsLoading(true);
setIsLoading(true);
// const integrationAuth = await mutateAsync({
// workspaceId: localStorage.getItem("projectData.id"),
// integration: "flyio",
// accessId: null,
// accessToken,
// url: null,
// namespace: null
// });
const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"),
integration: "gcp-secret-manager",
refreshToken: accessToken,
url: null,
namespace: null
});
// setIsLoading(false);
setIsLoading(false);
// router.push(`/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`);
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">
@ -54,10 +67,7 @@ export default function GCPSecretManagerAuthorizeIntegrationPage() {
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
// TODO: somehow get client ID
// 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}`;
}}
onClick={handleIntegrateWithOAuth}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-11 w-full mx-0 mt-4"
>
@ -74,13 +84,14 @@ export default function GCPSecretManagerAuthorizeIntegrationPage() {
isError={accessTokenErrorText !== "" ?? false}
>
<TextArea
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
placeholder=""
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
placeholder=""
className="h-48"
/>
</FormControl>
<Button
onClick={handleButtonClick}
onClick={handleIntegrateWithPAT}
color="mineshaft"
className="mt-4"
isLoading={isLoading}

View File

@ -0,0 +1,154 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
useCreateIntegration
} from "@app/hooks/api";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { useGetIntegrationAuthById, useGetIntegrationAuthApps } 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;

View File

@ -40,7 +40,6 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
let link = "";
switch (integrationOption.slug) {
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;
case "azure-key-vault":