mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 20:27:43 +00:00
Finish v1 Azure Key Vault integration
This commit is contained in:
@@ -13,10 +13,13 @@ const MONGO_URL = process.env.MONGO_URL!;
|
|||||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||||
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
|
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
|
||||||
const LOKI_HOST = process.env.LOKI_HOST || undefined;
|
const LOKI_HOST = process.env.LOKI_HOST || undefined;
|
||||||
|
const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!;
|
||||||
|
const TENANT_ID_AZURE = process.env.TENANT_ID_AZURE!;
|
||||||
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
||||||
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
||||||
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
||||||
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
|
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
|
||||||
|
const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!;
|
||||||
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
||||||
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
||||||
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
||||||
@@ -60,10 +63,13 @@ export {
|
|||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
VERBOSE_ERROR_OUTPUT,
|
VERBOSE_ERROR_OUTPUT,
|
||||||
LOKI_HOST,
|
LOKI_HOST,
|
||||||
|
CLIENT_ID_AZURE,
|
||||||
|
TENANT_ID_AZURE,
|
||||||
CLIENT_ID_HEROKU,
|
CLIENT_ID_HEROKU,
|
||||||
CLIENT_ID_VERCEL,
|
CLIENT_ID_VERCEL,
|
||||||
CLIENT_ID_NETLIFY,
|
CLIENT_ID_NETLIFY,
|
||||||
CLIENT_ID_GITHUB,
|
CLIENT_ID_GITHUB,
|
||||||
|
CLIENT_SECRET_AZURE,
|
||||||
CLIENT_SECRET_HEROKU,
|
CLIENT_SECRET_HEROKU,
|
||||||
CLIENT_SECRET_VERCEL,
|
CLIENT_SECRET_VERCEL,
|
||||||
CLIENT_SECRET_NETLIFY,
|
CLIENT_SECRET_NETLIFY,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const oAuthExchange = async (
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { workspaceId, code, integration } = req.body;
|
const { workspaceId, code, integration } = req.body;
|
||||||
|
|
||||||
if (!INTEGRATION_SET.has(integration))
|
if (!INTEGRATION_SET.has(integration))
|
||||||
throw new Error('Failed to validate integration');
|
throw new Error('Failed to validate integration');
|
||||||
|
|
||||||
@@ -40,12 +40,14 @@ export const oAuthExchange = async (
|
|||||||
throw new Error("Failed to get environments")
|
throw new Error("Failed to get environments")
|
||||||
}
|
}
|
||||||
|
|
||||||
await IntegrationService.handleOAuthExchange({
|
const integrationDetails = await IntegrationService.handleOAuthExchange({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
code,
|
code,
|
||||||
environment: environments[0].slug,
|
environment: environments[0].slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(integrationDetails);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -53,10 +55,6 @@ export const oAuthExchange = async (
|
|||||||
message: 'Failed to get OAuth2 code-token exchange'
|
message: 'Failed to get OAuth2 code-token exchange'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'Successfully enabled integration authorization'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import { eventPushSecrets } from '../../events';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createIntegration = async (req: Request, res: Response) => {
|
export const createIntegration = async (req: Request, res: Response) => {
|
||||||
|
|
||||||
|
// TODO: make this more versatile
|
||||||
|
|
||||||
let integration;
|
let integration;
|
||||||
try {
|
try {
|
||||||
// initialize new integration after saving integration access token
|
// initialize new integration after saving integration access token
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let action;
|
let action;
|
||||||
let integrationAuth;
|
let integrationAuth;
|
||||||
|
let newIntegration;
|
||||||
try {
|
try {
|
||||||
const bot = await Bot.findOne({
|
const bot = await Bot.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
@@ -100,7 +101,7 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initialize new integration after exchange
|
// initialize new integration after exchange
|
||||||
await new Integration({
|
newIntegration = await new Integration({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
app: null,
|
app: null,
|
||||||
@@ -113,6 +114,11 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to handle OAuth2 code-token exchange')
|
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
integrationAuth,
|
||||||
|
integration: newIntegration
|
||||||
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Sync/push environment variables in workspace with id [workspaceId] to
|
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/node';
|
|||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
import { IIntegrationAuth } from '../models';
|
import { IIntegrationAuth } from '../models';
|
||||||
import {
|
import {
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -40,6 +41,11 @@ const getApps = async ({
|
|||||||
let apps: App[];
|
let apps: App[];
|
||||||
try {
|
try {
|
||||||
switch (integrationAuth.integration) {
|
switch (integrationAuth.integration) {
|
||||||
|
case INTEGRATION_AZURE_KEY_VAULT:
|
||||||
|
apps = await getAppsAzureKeyVault({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
apps = await getAppsHeroku({
|
apps = await getAppsHeroku({
|
||||||
accessToken
|
accessToken
|
||||||
@@ -81,6 +87,15 @@ const getApps = async ({
|
|||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAppsAzureKeyVault = async ({
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
// TODO
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of apps for Heroku integration
|
* Return list of apps for Heroku integration
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
@@ -12,15 +14,27 @@ import {
|
|||||||
} from '../variables';
|
} from '../variables';
|
||||||
import {
|
import {
|
||||||
SITE_URL,
|
SITE_URL,
|
||||||
|
CLIENT_ID_AZURE,
|
||||||
CLIENT_ID_VERCEL,
|
CLIENT_ID_VERCEL,
|
||||||
CLIENT_ID_NETLIFY,
|
CLIENT_ID_NETLIFY,
|
||||||
CLIENT_ID_GITHUB,
|
CLIENT_ID_GITHUB,
|
||||||
|
CLIENT_SECRET_AZURE,
|
||||||
CLIENT_SECRET_HEROKU,
|
CLIENT_SECRET_HEROKU,
|
||||||
CLIENT_SECRET_VERCEL,
|
CLIENT_SECRET_VERCEL,
|
||||||
CLIENT_SECRET_NETLIFY,
|
CLIENT_SECRET_NETLIFY,
|
||||||
CLIENT_SECRET_GITHUB
|
CLIENT_SECRET_GITHUB
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
|
||||||
|
interface ExchangeCodeAzureResponse {
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
ext_expires_in: number;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
id_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ExchangeCodeHerokuResponse {
|
interface ExchangeCodeHerokuResponse {
|
||||||
token_type: string;
|
token_type: string;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
@@ -75,6 +89,11 @@ const exchangeCode = async ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
switch (integration) {
|
switch (integration) {
|
||||||
|
case INTEGRATION_AZURE_KEY_VAULT:
|
||||||
|
obj = await exchangeCodeAzure({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
break;
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
obj = await exchangeCodeHeroku({
|
obj = await exchangeCodeHeroku({
|
||||||
code
|
code
|
||||||
@@ -105,6 +124,46 @@ const exchangeCode = async ({
|
|||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken] for Azure OAuth2 code-token exchange
|
||||||
|
* @param param0
|
||||||
|
*/
|
||||||
|
const exchangeCodeAzure = async ({
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
let res: ExchangeCodeAzureResponse;
|
||||||
|
try {
|
||||||
|
res = (await axios.post(
|
||||||
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code,
|
||||||
|
scope: 'https://vault.azure.net/.default openid offline_access', // TODO: do we need all these permissions?
|
||||||
|
client_id: CLIENT_ID_AZURE,
|
||||||
|
client_secret: CLIENT_SECRET_AZURE,
|
||||||
|
redirect_uri: `${SITE_URL}/azure-key-vault`
|
||||||
|
} as any)
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(
|
||||||
|
accessExpiresAt.getSeconds() + res.expires_in
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||||
* OAuth2 code-token exchange
|
* OAuth2 code-token exchange
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { INTEGRATION_HEROKU } from '../variables';
|
import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
|
||||||
import {
|
import {
|
||||||
CLIENT_SECRET_HEROKU
|
SITE_URL,
|
||||||
|
CLIENT_ID_AZURE,
|
||||||
|
CLIENT_SECRET_AZURE,
|
||||||
|
CLIENT_SECRET_HEROKU
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import {
|
import {
|
||||||
INTEGRATION_HEROKU_TOKEN_URL
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
|
interface RefreshTokenAzureResponse {
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
ext_expires_in: 4871;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: 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]
|
||||||
@@ -25,6 +38,11 @@ const exchangeRefresh = async ({
|
|||||||
let accessToken;
|
let accessToken;
|
||||||
try {
|
try {
|
||||||
switch (integration) {
|
switch (integration) {
|
||||||
|
case INTEGRATION_AZURE_KEY_VAULT:
|
||||||
|
accessToken = await exchangeRefreshAzure({
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
accessToken = await exchangeRefreshHeroku({
|
accessToken = await exchangeRefreshHeroku({
|
||||||
refreshToken
|
refreshToken
|
||||||
@@ -40,6 +58,38 @@ const exchangeRefresh = async ({
|
|||||||
return accessToken;
|
return accessToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* Azure integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.refreshToken - refresh token to use to get new access token for Azure
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const exchangeRefreshAzure = async ({
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const res: RefreshTokenAzureResponse = (await axios.post(
|
||||||
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: CLIENT_ID_AZURE,
|
||||||
|
scope: 'openid offline_access',
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
client_secret: CLIENT_SECRET_AZURE
|
||||||
|
} as any)
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
return res.access_token;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get refresh OAuth2 access token for Azure');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
* Heroku integration
|
* Heroku integration
|
||||||
@@ -52,23 +102,23 @@ const exchangeRefreshHeroku = async ({
|
|||||||
}: {
|
}: {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
let accessToken;
|
|
||||||
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
|
let accessToken;
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
const res = await axios.post(
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
client_secret: CLIENT_SECRET_HEROKU
|
client_secret: CLIENT_SECRET_HEROKU
|
||||||
} as any)
|
} as any)
|
||||||
);
|
);
|
||||||
|
|
||||||
accessToken = res.data.access_token;
|
accessToken = res.data.access_token;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get new OAuth2 access token for Heroku');
|
throw new Error('Failed to refresh OAuth2 access token for Heroku');
|
||||||
}
|
}
|
||||||
|
|
||||||
return accessToken;
|
return accessToken;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import sodium from 'libsodium-wrappers';
|
|||||||
// const sodium = require('libsodium-wrappers');
|
// const sodium = require('libsodium-wrappers');
|
||||||
import { IIntegration, IIntegrationAuth } from '../models';
|
import { IIntegration, IIntegrationAuth } from '../models';
|
||||||
import {
|
import {
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -18,7 +19,6 @@ import {
|
|||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL
|
INTEGRATION_FLYIO_API_URL
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import { access, appendFile } from 'fs';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync/push [secrets] to [app] in integration named [integration]
|
* Sync/push [secrets] to [app] in integration named [integration]
|
||||||
@@ -41,6 +41,13 @@ const syncSecrets = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
switch (integration.integration) {
|
switch (integration.integration) {
|
||||||
|
case INTEGRATION_AZURE_KEY_VAULT:
|
||||||
|
await syncSecretsAzureKeyVault({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
await syncSecretsHeroku({
|
await syncSecretsHeroku({
|
||||||
integration,
|
integration,
|
||||||
@@ -93,6 +100,151 @@ const syncSecrets = async ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
* @param {String} obj.accessToken - access token for Azure Key Vault integration
|
||||||
|
*/
|
||||||
|
const syncSecretsAzureKeyVault = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
interface GetAzureKeyVaultSecret {
|
||||||
|
id: string; // secret URI
|
||||||
|
attributes: {
|
||||||
|
enabled: true,
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
recoveryLevel: string;
|
||||||
|
recoverableDays: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all secrets from Azure Key Vault by paginating through URL [url]
|
||||||
|
* @param {String} url - pagination URL to get next set of secrets from Azure Key Vault
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const paginateAzureKeyVaultSecrets = async (url: string) => {
|
||||||
|
let result: GetAzureKeyVaultSecret[] = [];
|
||||||
|
|
||||||
|
while (url) {
|
||||||
|
const res = await axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result = result.concat(res.data.value);
|
||||||
|
url = res.data.nextLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
|
||||||
|
|
||||||
|
let lastSlashIndex: number;
|
||||||
|
const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
||||||
|
if (!lastSlashIndex) {
|
||||||
|
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const azureKeyVaultSecret = await axios.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({
|
||||||
|
...azureKeyVaultSecret.data,
|
||||||
|
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1),
|
||||||
|
});
|
||||||
|
})))
|
||||||
|
.reduce((obj: any, secret: any) => ({
|
||||||
|
...obj,
|
||||||
|
[secret.key]: secret
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const setSecrets: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
Object.keys(secrets).forEach((key) => {
|
||||||
|
const hyphenatedKey = key.replace(/_/g, '-');
|
||||||
|
if (!(hyphenatedKey in res)) {
|
||||||
|
// case: secret has been created
|
||||||
|
setSecrets.push({
|
||||||
|
key: hyphenatedKey,
|
||||||
|
value: secrets[key]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (secrets[key] !== res[hyphenatedKey].value) {
|
||||||
|
// case: secret has been updated
|
||||||
|
setSecrets.push({
|
||||||
|
key: hyphenatedKey,
|
||||||
|
value: secrets[key]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteSecrets: AzureKeyVaultSecret[] = [];
|
||||||
|
|
||||||
|
Object.keys(res).forEach((key) => {
|
||||||
|
const underscoredKey = key.replace(/-/g, '_');
|
||||||
|
if (!(underscoredKey in secrets)) {
|
||||||
|
deleteSecrets.push(res[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync/push set secrets
|
||||||
|
if (setSecrets.length > 0) {
|
||||||
|
setSecrets.forEach(async ({ key, value }) => {
|
||||||
|
await axios.put(
|
||||||
|
`${integration.app}/secrets/${key}?api-version=7.3`,
|
||||||
|
{
|
||||||
|
value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteSecrets.length > 0) {
|
||||||
|
deleteSecrets.forEach(async (secret) => {
|
||||||
|
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to Azure Key Vault');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync/push [secrets] to Heroku app named [integration.app]
|
* Sync/push [secrets] to Heroku app named [integration.app]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -17,7 +18,7 @@ export interface IIntegration {
|
|||||||
owner: string;
|
owner: string;
|
||||||
targetEnvironment: string;
|
targetEnvironment: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
|
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
|
||||||
integrationAuth: Types.ObjectId;
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [
|
enum: [
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
export interface IIntegrationAuth {
|
export interface IIntegrationAuth {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
|
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
|
||||||
teamId: string;
|
teamId: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
refreshCiphertext?: string;
|
refreshCiphertext?: string;
|
||||||
@@ -31,6 +32,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [
|
enum: [
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
|
||||||
Integration
|
|
||||||
} from '../models';
|
|
||||||
import {
|
import {
|
||||||
handleOAuthExchangeHelper,
|
handleOAuthExchangeHelper,
|
||||||
syncIntegrationsHelper,
|
syncIntegrationsHelper,
|
||||||
@@ -10,7 +6,6 @@ import {
|
|||||||
setIntegrationAuthRefreshHelper,
|
setIntegrationAuthRefreshHelper,
|
||||||
setIntegrationAuthAccessHelper,
|
setIntegrationAuthAccessHelper,
|
||||||
} from '../helpers/integration';
|
} from '../helpers/integration';
|
||||||
import { exchangeCode } from '../integrations';
|
|
||||||
|
|
||||||
// should sync stuff be here too? Probably.
|
// should sync stuff be here too? Probably.
|
||||||
// TODO: move bot functions to IntegrationService.
|
// TODO: move bot functions to IntegrationService.
|
||||||
@@ -26,11 +21,15 @@ class IntegrationService {
|
|||||||
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||||
* - Add placeholder inactive integration
|
* - Add placeholder inactive integration
|
||||||
* - Create bot sequence for integration
|
* - Create bot sequence for integration
|
||||||
* @param {Object} obj
|
* @param {Object} obj1
|
||||||
* @param {String} obj.workspaceId - id of workspace
|
* @param {String} obj1.workspaceId - id of workspace
|
||||||
* @param {String} obj.environment - workspace environment
|
* @param {String} obj1.environment - workspace environment
|
||||||
* @param {String} obj.integration - name of integration
|
* @param {String} obj1.integration - name of integration
|
||||||
* @param {String} obj.code - code
|
* @param {String} obj1.code - code
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {IntegrationAuth} obj2.integrationAuth - integration authorization after OAuth2 code-token exchange
|
||||||
|
* @returns {Integration} obj2.integration - newly-initialized integration OAuth2 code-token exchange
|
||||||
|
* @retrun
|
||||||
*/
|
*/
|
||||||
static async handleOAuthExchange({
|
static async handleOAuthExchange({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -43,7 +42,7 @@ class IntegrationService {
|
|||||||
code: string;
|
code: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
}) {
|
}) {
|
||||||
await handleOAuthExchangeHelper({
|
return await handleOAuthExchangeHelper({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ENV_SET
|
ENV_SET
|
||||||
} from './environment';
|
} from './environment';
|
||||||
import {
|
import {
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
@@ -58,6 +60,7 @@ export {
|
|||||||
ENV_STAGING,
|
ENV_STAGING,
|
||||||
ENV_PROD,
|
ENV_PROD,
|
||||||
ENV_SET,
|
ENV_SET,
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -66,6 +69,7 @@ export {
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
CLIENT_ID_AZURE,
|
||||||
|
TENANT_ID_AZURE
|
||||||
|
} from '../config';
|
||||||
import {
|
import {
|
||||||
CLIENT_ID_HEROKU,
|
CLIENT_ID_HEROKU,
|
||||||
CLIENT_ID_NETLIFY,
|
CLIENT_ID_NETLIFY,
|
||||||
@@ -6,6 +10,7 @@ import {
|
|||||||
} from '../config';
|
} from '../config';
|
||||||
|
|
||||||
// integrations
|
// integrations
|
||||||
|
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
||||||
const INTEGRATION_HEROKU = 'heroku';
|
const INTEGRATION_HEROKU = 'heroku';
|
||||||
const INTEGRATION_VERCEL = 'vercel';
|
const INTEGRATION_VERCEL = 'vercel';
|
||||||
const INTEGRATION_NETLIFY = 'netlify';
|
const INTEGRATION_NETLIFY = 'netlify';
|
||||||
@@ -13,6 +18,7 @@ const INTEGRATION_GITHUB = 'github';
|
|||||||
const INTEGRATION_RENDER = 'render';
|
const INTEGRATION_RENDER = 'render';
|
||||||
const INTEGRATION_FLYIO = 'flyio';
|
const INTEGRATION_FLYIO = 'flyio';
|
||||||
const INTEGRATION_SET = new Set([
|
const INTEGRATION_SET = new Set([
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -25,6 +31,7 @@ const INTEGRATION_SET = new Set([
|
|||||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||||
|
|
||||||
// integration oauth endpoints
|
// integration oauth endpoints
|
||||||
|
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
|
||||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||||
const INTEGRATION_VERCEL_TOKEN_URL =
|
const INTEGRATION_VERCEL_TOKEN_URL =
|
||||||
'https://api.vercel.com/v2/oauth/access_token';
|
'https://api.vercel.com/v2/oauth/access_token';
|
||||||
@@ -40,6 +47,16 @@ const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
|
|||||||
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
|
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
|
||||||
|
|
||||||
const INTEGRATION_OPTIONS = [
|
const INTEGRATION_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: 'Azure Key Vault',
|
||||||
|
slug: 'azure-key-vault',
|
||||||
|
image: 'Microsoft Azure.png',
|
||||||
|
isAvailable: true,
|
||||||
|
type: 'oauth',
|
||||||
|
clientId: CLIENT_ID_AZURE,
|
||||||
|
tenantId: TENANT_ID_AZURE,
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Heroku',
|
name: 'Heroku',
|
||||||
slug: 'heroku',
|
slug: 'heroku',
|
||||||
@@ -143,6 +160,7 @@ const INTEGRATION_OPTIONS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -151,6 +169,7 @@ export {
|
|||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ interface Mapping {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const integrationSlugNameMapping: Mapping = {
|
||||||
|
'azure-key-vault': 'Azure Key Vault',
|
||||||
|
'heroku': 'Heroku',
|
||||||
|
'vercel': 'Vercel',
|
||||||
|
'netlify': 'Netlify',
|
||||||
|
'github': 'GitHub',
|
||||||
|
'render': 'Render',
|
||||||
|
'flyio': 'Fly.io'
|
||||||
|
}
|
||||||
|
|
||||||
const envMapping: Mapping = {
|
const envMapping: Mapping = {
|
||||||
Development: "dev",
|
Development: "dev",
|
||||||
Staging: "staging",
|
Staging: "staging",
|
||||||
@@ -49,6 +59,7 @@ const plans = plansProd || plansDev;
|
|||||||
export {
|
export {
|
||||||
contextNetlifyMapping,
|
contextNetlifyMapping,
|
||||||
envMapping,
|
envMapping,
|
||||||
|
integrationSlugNameMapping,
|
||||||
plans,
|
plans,
|
||||||
reverseContextNetlifyMapping,
|
reverseContextNetlifyMapping,
|
||||||
reverseEnvMapping}
|
reverseEnvMapping}
|
||||||
|
|||||||
@@ -199,13 +199,13 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
.split('/')
|
.split('/')
|
||||||
[router.asPath.split('/').length - 1].split('?')[0];
|
[router.asPath.split('/').length - 1].split('?')[0];
|
||||||
|
|
||||||
if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) {
|
if (!['heroku', 'vercel', 'github', 'netlify', 'azure-key-vault'].includes(intendedWorkspaceId)) {
|
||||||
localStorage.setItem('projectData.id', intendedWorkspaceId);
|
localStorage.setItem('projectData.id', intendedWorkspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||||
if (
|
if (
|
||||||
!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
|
!['heroku', 'vercel', 'github', 'netlify', 'azure-key-vault'].includes(intendedWorkspaceId) &&
|
||||||
!userWorkspaces
|
!userWorkspaces
|
||||||
.map((workspace: { _id: string }) => workspace._id)
|
.map((workspace: { _id: string }) => workspace._id)
|
||||||
.includes(intendedWorkspaceId)
|
.includes(intendedWorkspaceId)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
|
|||||||
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
|
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
// TODO: This needs to be moved from public folder
|
// TODO: This needs to be moved from public folder
|
||||||
import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
|
import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
|
||||||
|
|
||||||
import Button from '@app/components/basic/buttons/Button';
|
import Button from '@app/components/basic/buttons/Button';
|
||||||
import ListBox from '@app/components/basic/Listbox';
|
import ListBox from '@app/components/basic/Listbox';
|
||||||
@@ -52,6 +52,7 @@ const IntegrationTile = ({
|
|||||||
environments = [],
|
environments = [],
|
||||||
handleDeleteIntegration
|
handleDeleteIntegration
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
|
||||||
// set initial environment. This find will only execute when component is mounting
|
// set initial environment. This find will only execute when component is mounting
|
||||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||||
environments.find(({ slug }) => slug === integration.environment) || {
|
environments.find(({ slug }) => slug === integration.environment) || {
|
||||||
@@ -72,7 +73,14 @@ const IntegrationTile = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setApps(tempApps);
|
setApps(tempApps);
|
||||||
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
|
|
||||||
|
if (integration?.app) {
|
||||||
|
setIntegrationApp(integration.app);
|
||||||
|
} else if (tempApps.length > 0) {
|
||||||
|
setIntegrationApp(tempApps[0].name)
|
||||||
|
} else {
|
||||||
|
setIntegrationApp('');
|
||||||
|
}
|
||||||
|
|
||||||
switch (integration.integration) {
|
switch (integration.integration) {
|
||||||
case 'vercel':
|
case 'vercel':
|
||||||
@@ -174,7 +182,7 @@ const IntegrationTile = ({
|
|||||||
return <div />;
|
return <div />;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!integrationApp || apps.length === 0) return <div />;
|
if (!integrationApp) return <div />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
||||||
@@ -201,7 +209,8 @@ const IntegrationTile = ({
|
|||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
||||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||||
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
|
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
|
||||||
|
{integrationSlugNameMapping[integration.integration]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
|
|||||||
// ws empty means user has no access to the ws
|
// ws empty means user has no access to the ws
|
||||||
// push to the first workspace
|
// push to the first workspace
|
||||||
if (!isLoading && !value?.currentWorkspace?._id) {
|
if (!isLoading && !value?.currentWorkspace?._id) {
|
||||||
router.push(`/dashboard/${value.workspaces?.[0]?._id}`);
|
// router.push(`/dashboard/${value.workspaces?.[0]?._id}`);
|
||||||
}
|
}
|
||||||
}, [value?.currentWorkspace?._id, isLoading, value.workspaces?.[0]?._id, router.pathname]);
|
}, [value?.currentWorkspace?._id, isLoading, value.workspaces?.[0]?._id, router.pathname]);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
|
|||||||
})
|
})
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
return res;
|
return (res.json());
|
||||||
}
|
}
|
||||||
console.log('Failed to authorize the integration');
|
console.log('Failed to authorize the integration');
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import queryString from 'query-string';
|
||||||
|
|
||||||
|
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from '../components/v2';
|
||||||
|
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
|
||||||
|
import updateIntegration from './api/integrations/updateIntegration';
|
||||||
|
import getAWorkspace from './api/workspace/getAWorkspace';
|
||||||
|
|
||||||
|
interface Integration {
|
||||||
|
_id: string;
|
||||||
|
isActive: boolean;
|
||||||
|
app: string | null;
|
||||||
|
appId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
environment: string;
|
||||||
|
integration: string;
|
||||||
|
targetEnvironment: string;
|
||||||
|
workspace: string;
|
||||||
|
integrationAuth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AzureKeyVault() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// query-string variables
|
||||||
|
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
|
||||||
|
const {code} = parsedUrl;
|
||||||
|
const {state} = parsedUrl;
|
||||||
|
|
||||||
|
const [integration, setIntegration] = useState<Integration | null>(null);
|
||||||
|
const [environments, setEnvironments] = useState<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
const [environment, setEnvironment] = useState('');
|
||||||
|
const [vaultBaseUrl, setVaultBaseUrl] = useState('');
|
||||||
|
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (state === localStorage.getItem('latestCSRFToken')) {
|
||||||
|
localStorage.removeItem('latestCSRFToken');
|
||||||
|
|
||||||
|
const integrationDetails = await AuthorizeIntegration({
|
||||||
|
workspaceId: localStorage.getItem('projectData.id') as string,
|
||||||
|
code: code as string,
|
||||||
|
integration: 'azure-key-vault',
|
||||||
|
});
|
||||||
|
|
||||||
|
setIntegration(integrationDetails.integration);
|
||||||
|
|
||||||
|
const workspaceId = localStorage.getItem('projectData.id');
|
||||||
|
if (!workspaceId) return;
|
||||||
|
|
||||||
|
const workspace = await getAWorkspace(workspaceId);
|
||||||
|
setEnvironment(workspace.environments[0].slug);
|
||||||
|
setEnvironments(workspace.environments);
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Azure Key Vault integration error: ', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (vaultBaseUrl.length === 0) {
|
||||||
|
setVaultBaseUrlErrorText('Vault URI cannot be blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!vaultBaseUrl.startsWith('https://')
|
||||||
|
|| !vaultBaseUrl.endsWith('vault.azure.net')
|
||||||
|
) {
|
||||||
|
setVaultBaseUrlErrorText('Vault URI must be like https://<vault_name>.vault.azure.net');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!integration) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
await updateIntegration({
|
||||||
|
integrationId: integration._id,
|
||||||
|
isActive: true,
|
||||||
|
environment,
|
||||||
|
app: vaultBaseUrl,
|
||||||
|
appId: null,
|
||||||
|
targetEnvironment: null,
|
||||||
|
owner: null
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
`/integrations/${localStorage.getItem('projectData.id')}`
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (integration && environments.length > 0) ? (
|
||||||
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
<Card className="max-w-md p-8 rounded-md">
|
||||||
|
<CardTitle className='text-center'>Azure Key Vault Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
className='mt-4'
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={environment}
|
||||||
|
onValueChange={(val) => setEnvironment(val)}
|
||||||
|
className='w-full border border-mineshaft-500'
|
||||||
|
>
|
||||||
|
{environments.map((e) => (
|
||||||
|
<SelectItem value={e.slug} key={`azure-key-vault-environment-${e.slug}`}>
|
||||||
|
{e.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Vault URI"
|
||||||
|
errorText={vaultBaseUrlErrorText}
|
||||||
|
isError={vaultBaseUrlErrorText !== '' ?? false}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder='https://example.vault.azure.net'
|
||||||
|
value={vaultBaseUrl}
|
||||||
|
onChange={(e) => setVaultBaseUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className='mt-4'
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : <div />
|
||||||
|
}
|
||||||
|
|
||||||
|
AzureKeyVault.requireAuth = true;
|
||||||
|
|
||||||
|
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
|
||||||
@@ -25,7 +25,7 @@ export default function Github() {
|
|||||||
integration: 'github',
|
integration: 'github',
|
||||||
});
|
});
|
||||||
router.push(
|
router.push(
|
||||||
`/integrations/${ localStorage.getItem('projectData.id')}`
|
`/integrations/${localStorage.getItem('projectData.id')}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -195,6 +195,11 @@ export default function Integrations() {
|
|||||||
localStorage.setItem('latestCSRFToken', state);
|
localStorage.setItem('latestCSRFToken', state);
|
||||||
|
|
||||||
switch (integrationOption.slug) {
|
switch (integrationOption.slug) {
|
||||||
|
case 'azure-key-vault':
|
||||||
|
window.location.assign(
|
||||||
|
`https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/azure-key-vault&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
case 'heroku':
|
case 'heroku':
|
||||||
window.location.assign(
|
window.location.assign(
|
||||||
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
|
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
|
||||||
@@ -275,10 +280,15 @@ export default function Integrations() {
|
|||||||
|
|
||||||
// case: integration has been authorized before
|
// case: integration has been authorized before
|
||||||
// -> create new integration
|
// -> create new integration
|
||||||
const integration = await createIntegration({
|
|
||||||
integrationAuthId: integrationAuthX._id
|
if (!['azure-key-vault'].includes(integrationOption.slug)) {
|
||||||
});
|
const integration = await createIntegration({
|
||||||
setIntegrations([...integrations, integration]);
|
integrationAuthId: integrationAuthX._id
|
||||||
|
});
|
||||||
|
setIntegrations([...integrations, integration]);
|
||||||
|
} else {
|
||||||
|
handleIntegrationOption({ integrationOption });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user