Finish preliminary Vault integration, made docs for Vault and Checkly

This commit is contained in:
Tuan Dang
2023-06-09 15:36:37 +01:00
parent c51b194ba6
commit 00dfcfcf4e
48 changed files with 680 additions and 63 deletions

View File

@@ -86,47 +86,53 @@ export const saveIntegrationAccessToken = async (
// TODO: check if access token is valid for each integration
let integrationAuth;
const {
workspaceId,
accessId,
accessToken,
integration
}: {
workspaceId: string;
accessId: string | null;
accessToken: string;
integration: string;
} = req.body;
const {
workspaceId,
accessId,
accessToken,
url,
namespace,
integration
}: {
workspaceId: string;
accessId: string | null;
accessToken: string;
url: string;
namespace: string;
integration: string;
} = req.body;
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
if (!bot) throw new Error('Bot must be enabled to save integration access token');
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId),
integration
}, {
workspace: new Types.ObjectId(workspaceId),
integration,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true,
upsert: true
});
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId),
integration
}, {
workspace: new Types.ObjectId(workspaceId),
integration,
url,
namespace,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true,
upsert: true
});
// encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId,
accessToken,
accessExpiresAt: undefined
});
// encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId,
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');
return res.status(200).send({
integrationAuth

View File

@@ -57,6 +57,7 @@ export const createIntegration = async (req: Request, res: Response) => {
})
});
}
return res.status(200).send({
integration,
});

View File

@@ -36,7 +36,8 @@ import {
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL
INTEGRATION_CHECKLY_API_URL,
INTEGRATION_HASHICORP_VAULT
} from "../variables";
import { standardRequest} from '../config/request';
@@ -200,6 +201,15 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_HASHICORP_VAULT:
await syncSecretsHashiCorpVault({
integration,
integrationAuth,
secrets,
accessId,
accessToken
});
break;
}
};
@@ -1762,5 +1772,65 @@ const syncSecretsCheckly = async ({
}
};
/**
* Sync/push [secrets] to HashiCorp Vault path
* @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 HashiCorp Vault integration
*/
const syncSecretsHashiCorpVault = async ({
integration,
integrationAuth,
secrets,
accessId,
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
if (!accessId) return;
interface LoginAppRoleRes {
auth: {
client_token: string;
}
}
// get Vault client token (could be optimized)
const { data }: { data: LoginAppRoleRes } = await standardRequest.post(
`${integrationAuth.url}/v1/auth/approle/login`,
{
"role_id": accessId,
"secret_id": accessToken
},
{
headers: {
"X-Vault-Namespace": integrationAuth.namespace
}
}
);
const clientToken = data.auth.client_token;
await standardRequest.post(
`${integrationAuth.url}/v1/${integration.app}/data/${integration.path}`,
{
data: secrets
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
"Content-Type": "application/json",
"X-Vault-Token": clientToken,
"X-Vault-Namespace": integrationAuth.namespace
},
}
);
};
export { syncSecrets };

View File

@@ -3,6 +3,7 @@ import { ErrorRequestHandler } from 'express';
import { InternalServerError } from '../utils/errors';
import { getLogger } from '../utils/logger';
import RequestError, { LogLevel } from '../utils/requestError';
import { getNodeEnv } from '../config';
export const requestErrorHandler: ErrorRequestHandler = async (
error: RequestError | Error,
@@ -12,6 +13,11 @@ export const requestErrorHandler: ErrorRequestHandler = async (
) => {
if (res.headersSent) return next();
if (await getNodeEnv() !== "production") {
/* eslint-disable no-console */
console.error(error);
}
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
if (!(error instanceof RequestError)) {
error = InternalServerError({

View File

@@ -14,7 +14,8 @@ import {
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT
} from "../variables";
export interface IIntegration {
@@ -47,7 +48,8 @@ export interface IIntegration {
| 'circleci'
| 'travisci'
| 'supabase'
| 'checkly';
| 'checkly'
| 'hashicorp-vault';
integrationAuth: Types.ObjectId;
}
@@ -133,7 +135,8 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT
],
required: true,
},

View File

@@ -14,6 +14,7 @@ import {
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
@@ -25,6 +26,8 @@ export interface IIntegrationAuth extends Document {
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly';
teamId: string;
accountId: string;
url: string;
namespace: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
@@ -62,7 +65,8 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE
INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT
],
required: true,
},
@@ -70,6 +74,14 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
// 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,

View File

@@ -15,7 +15,7 @@ import {
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration for integration auth
router.post(
'/',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]

View File

@@ -57,6 +57,8 @@ router.post(
body('workspaceId').exists().trim().notEmpty(),
body('accessId').trim(),
body('accessToken').exists().trim().notEmpty(),
body('url').trim(),
body('namespace').trim(),
body('integration').exists().trim().notEmpty(),
validateRequest,
requireAuth({

View File

@@ -23,6 +23,7 @@ export const INTEGRATION_CIRCLECI = "circleci";
export const INTEGRATION_TRAVISCI = "travisci";
export const INTEGRATION_SUPABASE = 'supabase';
export const INTEGRATION_CHECKLY = 'checkly';
export const INTEGRATION_HASHICORP_VAULT = 'hashicorp-vault';
export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@@ -35,7 +36,8 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT
]);
// integration types
@@ -202,6 +204,15 @@ export const getIntegrationOptions = async () => {
clientId: '',
docsLink: ''
},
{
name: 'HashiCorp Vault',
slug: 'hashicorp-vault',
image: 'Vault.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Google Cloud Platform',
slug: 'gcp',

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -0,0 +1,37 @@
---
title: "Checkly"
description: "How to sync secrets from Infisical to Checkly"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Enter your Checkly API Key
Obtain a Checkly API Key in User Settings > API Keys.
![integrations checkly dashboard](../../images/integrations-checkly-dashboard.png)
![integrations checkly token](../../images/integrations-checkly-token.png)
Press on the Checkly tile and input your Checkly API Key to grant Infisical access to your Checkly account.
![integrations checkly authorization](../../images/integrations-checkly-auth.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 Checkly press create integration to start syncing secrets.
![integrations checkly](../../images/integrations-checkly-create.png)
![integrations checkly](../../images/integrations-checkly.png)

View File

@@ -0,0 +1,159 @@
---
title: "HashiCorp Vault"
description: "How to sync secrets from Infisical to HashiCorp Vault"
---
<Note>
Infisical connects to Vault via the AppRole auth method.
Currently, each Infisical project can only point and sync secrets to one Vault cluster / namespace
but with unlimited integrations to different paths within it.
This tutorial makes use of Vault's UI but, in principle, instructions can executed via
Vault CLI or API call.
Lastly, you should note that we provide a simple use-case and, in practice, you should adapt and extend it to your own Vault use-case and follow best practices, for instance when defining fine-grained ACL policies.
</Note>
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Have experience with [HashiCorp Vault](https://www.vaultproject.io/).
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Prepare Vault
This section mirrors the latter parts of the [Vault quickstart](https://developer.hashicorp.com/vault/tutorials/cloud/getting-started-intro) provided by HashiCorp and uses sample names/values for demonstration.
To begin, navigate to the cluster / namespace that you want to sync secrets to in Vault; we'll use the default `admin` namespace (in practice, we recommend creating a namespace and not using the default `admin` namespace).
### Enable KV Secrets Engine
In Secrets, enable a KV Secrets Engine at a path for Infisical to sync secrets to; we'll use the path `kv`.
![integrations hashicorp vault secrets engine](../../images/integrations-hashicorp-vault-engine-1.png)
![integrations hashicorp vault secrets engine](../../images/integrations-hashicorp-vault-engine-2.png)
![integrations hashicorp vault secrets engine](../../images/integrations-hashicorp-vault-engine-3.png)
### Enable the AppRole auth method
In Access > Auth Methods, enable the AppRole auth method.
![integrations hashicorp vault access](../../images/integrations-hashicorp-vault-access-1.png)
![integrations hashicorp vault access](../../images/integrations-hashicorp-vault-access-2.png)
![integrations hashicorp vault access](../../images/integrations-hashicorp-vault-access-3.png)
### Create an ACL Policy
Now in Policies, create a new ACL policy scoped to the path(s) you wish Infisical to be able to sync secrets to.
We'll call the policy `test` and have it grant access to the `dev` path in the KV Secrets Engine where we will be syncing secrets to from Infisical.
```console
path "kv/data/dev" {
capabilities = [ "create", "read", "update" ]
}
path "sys/namespaces/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
```
<Note>
`kv` comes from the path of the KV Secrets Engine that we enabled and `dev` is the chosen path within it
that we want to sync secrets to.
</Note>
![integrations hashicorp vault policy](../../images/integrations-hashicorp-vault-policy-1.png)
![integrations hashicorp vault policy](../../images/integrations-hashicorp-vault-policy-2.png)
![integrations hashicorp vault policy](../../images/integrations-hashicorp-vault-policy-3.png)
### Create a role with the policy attached
We now create a `infisical` role with the generated token's time-to-live (TTL) set to 1 hour and can be renewed for up to 4 hours from the time of its creation.
1. Click the Vault CLI shell icon (`>_`) to open a command shell in the browser.
![integrations hashicorp vault shell](../../images/integrations-hashicorp-vault-shell.png)
2. Copy the command below.
```console
vault write auth/approle/role/infisical token_policies="test" token_ttl=1h token_max_ttl=4h
```
3. Paste the command into the command shell in the browser and press the enter button.
### Generate a RoleID and SecretID
Finally, we need to generate a **RoleID** and **SecretID** (like a username and password) that Infisical can use
to authenticate with Vault.
1. Click the Vault CLI shell icon (>_) again to open a command shell.
2. Read the RoleID.
```console
vault read auth/approle/role/infisical/role-id
```
Example output:
```console
Key Value
role_id b6ccdcca-183b-ce9c-6b98-b556b9a0edb9
```
3. Generate a new SecretID of the `infisical` role.
```console
vault write -force auth/approle/role/infisical/secret-id
```
Example output:
```console
Key Value
secret_id 735a47cc-7a98-77cc-0128-12b1e96a4157
secret_id_accessor 3ab305d1-1eab-df4b-4079-ef7135635c49
...snip...
```
Great. We're now ready to connect Infisical to Vault!
## Enter your Vault instance and authentication details
Back in Infisical, press on the HashiCorp Vault tile and input your Vault instance and `infisical` role RoleID and SecretID.
![integrations hashicorp vault authorization](../../images/integrations-hashicorp-vault-auth.png)
For additional details on each field:
- Vault Cluster URL: The address of your cluster, either HCP or self-hosted.
If using HCP, you can copy your Cluster URL in the Cluster Overview:
- Vault Namespace: The Vault namespace you wish to connect to.
- Vault RoleID: The RoleID previously created for the `infisical` role.
- Vault SecretID: The SecretID previously created for the `infisical` role.
## Start integration
Select which Infisical environment secrets you want to sync to Vault.
For additional details on each field:
- Vault KV Secrets Engine Path: the path at which you enabled the intended KV Secrets Engine; in this demonstration, we used `kv`.
- Vault Secret(s) Path: the path in the KV Secrets Engine that you wish to sync secrets to.
Press create integration to start syncing secrets to Vault.
![integrations hashicorp vault](../../images/integrations-hashicorp-vault-create.png)
![integrations hashicorp vault](../../images/integrations-hashicorp-vault.png)

View File

@@ -21,6 +21,8 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [Railway](/integrations/cloud/railway) | Cloud | Available |
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
| [Checkly](/integrations/cloud/checkly) | Cloud | Available |
| [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available |
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available |

View File

@@ -197,6 +197,8 @@
"integrations/cloud/railway",
"integrations/cloud/flyio",
"integrations/cloud/supabase",
"integrations/cloud/checkly",
"integrations/cloud/hashicorp-vault",
"integrations/cloud/azure-key-vault",
"integrations/cicd/githubactions",
"integrations/cicd/gitlab",

View File

@@ -17,7 +17,8 @@ const integrationSlugNameMapping: Mapping = {
'circleci': 'CircleCI',
'travisci': 'TravisCI',
'supabase': 'Supabase',
'checkly': 'Checkly'
'checkly': 'Checkly',
'hashicorp-vault': 'Vault'
}
const envMapping: Mapping = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -213,6 +213,8 @@ const IntegrationTile = ({
};
if (!integrationApp && integration.integration !== "checkly") return <div />;
const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp;
return (
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-mineshaft-800 border border-mineshaft-600 p-6">
@@ -245,13 +247,15 @@ const IntegrationTile = ({
</div>
<div className="mr-2">
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
{integrationApp ? <div title={integrationApp}><ListBox
{integrationApp ? <div title={integrationApp}>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp}
isSelected={isSelected}
onChange={(app) => {
setIntegrationApp(app);
}}
/></div> : <div className='w-52 h-10 rounded-md bg-mineshaft-600 animate-pulse px-4 font-bold py-2'>-</div>}
/>
</div> : <div className='w-52 h-10 rounded-md bg-mineshaft-600 animate-pulse px-4 font-bold py-2'>-</div>}
</div>
{renderIntegrationSpecificParams(integration)}
</div>

View File

@@ -1,10 +1,12 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
workspaceId: string | null;
integration: string | undefined;
accessId: string | null;
accessToken: string;
workspaceId: string | null;
integration: string | undefined;
accessId: string | null;
accessToken: string;
url: string | null;
namespace: string | null;
}
/**
* This route creates a new integration authorization for integration [integration]
@@ -15,13 +17,17 @@ interface Props {
* @param {String} obj.workspaceId - id of workspace to authorize integration for
* @param {String} obj.integration - integration
* @param {String} obj.accessToken - access token to save
* @param {String} obj.url - URL of the Vault instance
* @param {String} obj.namespace - Vault-specific namespace param
* @returns
*/
const saveIntegrationAccessToken = ({
workspaceId,
integration,
accessId,
accessToken
accessToken,
url,
namespace
}: Props) =>
SecurityClient.fetchCall(`/api/v1/integration-auth/access-token`, {
method: 'POST',
@@ -32,7 +38,9 @@ const saveIntegrationAccessToken = ({
workspaceId,
integration,
accessId,
accessToken
accessToken,
url,
namespace
})
}).then(async (res) => {
if (res && res.status === 200) {

View File

@@ -216,6 +216,9 @@ export default function Integrations() {
case 'railway':
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case 'hashicorp-vault':
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
default:
break;
}
@@ -277,6 +280,9 @@ export default function Integrations() {
case 'railway':
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'hashicorp-vault':
link = `${window.location.origin}/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}

View File

@@ -34,7 +34,9 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'aws-parameter-store',
accessId: accessKey,
accessToken: accessSecretKey
accessToken: accessSecretKey,
url: null,
namespace: null
});
setAccessKey('');
@@ -58,7 +60,11 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() {
errorText={accessKeyErrorText}
isError={accessKeyErrorText !== '' ?? false}
>
<Input placeholder="" value={accessKey} onChange={(e) => setAccessKey(e.target.value)} />
<Input
placeholder=""
value={accessKey}
onChange={(e) => setAccessKey(e.target.value)}
/>
</FormControl>
<FormControl
label="Secret Access Key"

View File

@@ -34,7 +34,9 @@ export default function AWSSecretManagerCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'aws-secret-manager',
accessId: accessKey,
accessToken: accessSecretKey
accessToken: accessSecretKey,
url: null,
namespace: null
});
setAccessKey('');

View File

@@ -24,7 +24,9 @@ export default function ChecklyCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'checkly',
accessId: null,
accessToken
accessToken,
url: null,
namespace: null
});
setIsLoading(false);

View File

@@ -24,7 +24,9 @@ export default function CircleCICreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'circleci',
accessToken: apiKey,
accessId: null
accessId: null,
url: null,
namespace:null
});
setIsLoading(false);

View File

@@ -24,7 +24,9 @@ export default function FlyioCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'flyio',
accessId: null,
accessToken
accessToken,
url: null,
namespace: null
});
setIsLoading(false);

View File

@@ -0,0 +1,129 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { Button, Card, CardTitle, FormControl, Input } from '../../../components/v2';
import saveIntegrationAccessToken from '../../api/integrations/saveIntegrationAccessToken';
export default function HashiCorpVaultAuthorizeIntegrationPage() {
const router = useRouter();
const [vaultURL, setVaultURL] = useState('');
const [vaultURLErrorText, setVaultURLErrorText] = useState('');
const [vaultNamespace, setVaultNamespace] = useState('');
const [vaultNamespaceErrorText, setVaultNamespaceErrorText] = useState('');
const [vaultRoleID, setVaultRoleID] = useState('');
const [vaultRoleIDErrorText, setVaultRoleIDErrorText] = useState('');
const [vaultSecretID, setVaultSecretID] = useState('');
const [vaultSecretIDErrorText, setVaultSecretIDErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
if (vaultURL.length === 0) {
setVaultURLErrorText('Vault Cluster URL cannot be blank');
} else {
setVaultURLErrorText('');
}
if (vaultNamespace.length === 0) {
setVaultNamespaceErrorText('Vault Namespace cannot be blank');
} else {
setVaultNamespaceErrorText('');
}
if (vaultRoleID.length === 0) {
setVaultRoleIDErrorText('Vault Role ID cannot be blank');
} else {
setVaultRoleIDErrorText('');
}
if (vaultSecretID.length === 0) {
setVaultSecretIDErrorText('Vault Secret ID cannot be blank');
} else {
setVaultSecretIDErrorText('');
}
if (
vaultURL.length === 0 ||
vaultNamespace.length === 0 ||
vaultRoleID.length === 0 ||
vaultSecretID.length === 0
) {
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'hashicorp-vault',
accessId: vaultRoleID,
accessToken: vaultSecretID,
url: vaultURL,
namespace: vaultNamespace
});
setIsLoading(false);
router.push(`/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
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">Vault Integration</CardTitle>
<FormControl
label="Vault Cluster URL"
errorText={vaultURLErrorText}
isError={vaultURLErrorText !== '' ?? false}
>
<Input placeholder="" value={vaultURL} onChange={(e) => setVaultURL(e.target.value)} />
</FormControl>
<FormControl
label="Vault Namespace"
errorText={vaultNamespaceErrorText}
isError={vaultNamespaceErrorText !== '' ?? false}
>
<Input
placeholder="admin/education"
value={vaultNamespace}
onChange={(e) => setVaultNamespace(e.target.value)}
/>
</FormControl>
<FormControl
label="Vault RoleID"
errorText={vaultRoleIDErrorText}
isError={vaultRoleIDErrorText !== '' ?? false}
>
<Input placeholder="" value={vaultRoleID} onChange={(e) => setVaultRoleID(e.target.value)} />
</FormControl>
<FormControl
label="Vault SecretID"
errorText={vaultSecretIDErrorText}
isError={vaultSecretIDErrorText !== '' ?? false}
>
<Input
placeholder=""
value={vaultSecretID}
onChange={(e) => setVaultSecretID(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Vault
</Button>
</Card>
</div>
);
}
HashiCorpVaultAuthorizeIntegrationPage.requireAuth = true;

View File

@@ -0,0 +1,136 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Input, Select, SelectItem } from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from '../../api/integrations/createIntegration';
export default function HashiCorpVaultCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [vaultEnginePath, setVaultEnginePath] = useState('');
const [vaultEnginePathErrorText, setVaultEnginePathErrorText ] = useState('');
const [vaultSecretPath, setVaultSecretPath] = useState('');
const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false);
const isValidVaultPath = (secretPath: string) => {
return !(
secretPath.length === 0 ||
secretPath.startsWith('/') ||
secretPath.endsWith('/')
);
};
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
if (!isValidVaultPath(vaultEnginePath)) {
setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv');
} else {
setVaultEnginePathErrorText('');
}
if (!isValidVaultPath(vaultSecretPath)) {
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
} else {
setVaultSecretPathErrorText('');
}
if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: vaultEnginePath,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: vaultSecretPath,
region: null
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem('projectData.id')}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth && workspace ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Vault 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={`vault-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vault KV Secrets Engine Path"
errorText={vaultEnginePathErrorText}
isError={vaultEnginePathErrorText !== '' ?? false}
>
<Input
placeholder="kv"
value={vaultEnginePath}
onChange={(e) => setVaultEnginePath(e.target.value)}
/>
</FormControl>
<FormControl
label="Vault Secret(s) Path"
errorText={vaultSecretPathErrorText}
isError={vaultSecretPathErrorText !== '' ?? false}
>
<Input
placeholder="machine/dev"
value={vaultSecretPath}
onChange={(e) => setVaultSecretPath(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
HashiCorpVaultCreateIntegrationPage.requireAuth = true;

View File

@@ -24,7 +24,9 @@ export default function RailwayAuthorizeIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'railway',
accessId: null,
accessToken: apiKey
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);

View File

@@ -24,7 +24,9 @@ export default function RenderCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'render',
accessId: null,
accessToken: apiKey
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);

View File

@@ -24,7 +24,9 @@ export default function SupabaseCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'supabase',
accessToken: apiKey,
accessId: null
accessId: null,
url: null,
namespace: null
});
setIsLoading(false);

View File

@@ -24,7 +24,9 @@ export default function TravisCICreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'travisci',
accessToken: apiKey,
accessId: null
accessId: null,
url: null,
namespace: null
});
setIsLoading(false);