diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index a002187c8..fc79e22cf 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -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 diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 83f7c784a..5119761d0 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -57,6 +57,7 @@ export const createIntegration = async (req: Request, res: Response) => { }) }); } + return res.status(200).send({ integration, }); diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 3b655e2fd..76d009944 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -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 }; diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 08aa0d5fd..6aa73954b 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -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({ diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 504d4a2be..d4fbae807 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -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( INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, - INTEGRATION_CHECKLY + INTEGRATION_CHECKLY, + INTEGRATION_HASHICORP_VAULT ], required: true, }, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 4f2209f2d..cc28c9fd6 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -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( INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, - INTEGRATION_SUPABASE + INTEGRATION_SUPABASE, + INTEGRATION_HASHICORP_VAULT ], required: true, }, @@ -70,6 +74,14 @@ const integrationAuthSchema = new Schema( // 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, diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 52930ce0c..b8e0b38bd 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -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] diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index fa5863029..f8fe15a4b 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -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({ diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 7fa8f6a23..26e9f2544 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -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', diff --git a/docs/images/integrations-checkly-auth.png b/docs/images/integrations-checkly-auth.png new file mode 100644 index 000000000..b6299aefe Binary files /dev/null and b/docs/images/integrations-checkly-auth.png differ diff --git a/docs/images/integrations-checkly-create.png b/docs/images/integrations-checkly-create.png new file mode 100644 index 000000000..271ca1a9c Binary files /dev/null and b/docs/images/integrations-checkly-create.png differ diff --git a/docs/images/integrations-checkly-dashboard.png b/docs/images/integrations-checkly-dashboard.png new file mode 100644 index 000000000..d600ecc23 Binary files /dev/null and b/docs/images/integrations-checkly-dashboard.png differ diff --git a/docs/images/integrations-checkly-token.png b/docs/images/integrations-checkly-token.png new file mode 100644 index 000000000..80f7bcf19 Binary files /dev/null and b/docs/images/integrations-checkly-token.png differ diff --git a/docs/images/integrations-checkly.png b/docs/images/integrations-checkly.png new file mode 100644 index 000000000..4734d5257 Binary files /dev/null and b/docs/images/integrations-checkly.png differ diff --git a/docs/images/integrations-hashicorp-vault-access-1.png b/docs/images/integrations-hashicorp-vault-access-1.png new file mode 100644 index 000000000..367386709 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-access-1.png differ diff --git a/docs/images/integrations-hashicorp-vault-access-2.png b/docs/images/integrations-hashicorp-vault-access-2.png new file mode 100644 index 000000000..80de8df26 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-access-2.png differ diff --git a/docs/images/integrations-hashicorp-vault-access-3.png b/docs/images/integrations-hashicorp-vault-access-3.png new file mode 100644 index 000000000..d51142541 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-access-3.png differ diff --git a/docs/images/integrations-hashicorp-vault-auth.png b/docs/images/integrations-hashicorp-vault-auth.png new file mode 100644 index 000000000..b587777f9 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-auth.png differ diff --git a/docs/images/integrations-hashicorp-vault-create.png b/docs/images/integrations-hashicorp-vault-create.png new file mode 100644 index 000000000..7fdef0d4a Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-create.png differ diff --git a/docs/images/integrations-hashicorp-vault-engine-1.png b/docs/images/integrations-hashicorp-vault-engine-1.png new file mode 100644 index 000000000..a65870b44 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-engine-1.png differ diff --git a/docs/images/integrations-hashicorp-vault-engine-2.png b/docs/images/integrations-hashicorp-vault-engine-2.png new file mode 100644 index 000000000..34b03768f Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-engine-2.png differ diff --git a/docs/images/integrations-hashicorp-vault-engine-3.png b/docs/images/integrations-hashicorp-vault-engine-3.png new file mode 100644 index 000000000..624fdc574 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-engine-3.png differ diff --git a/docs/images/integrations-hashicorp-vault-policy-1.png b/docs/images/integrations-hashicorp-vault-policy-1.png new file mode 100644 index 000000000..e2a77654a Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-policy-1.png differ diff --git a/docs/images/integrations-hashicorp-vault-policy-2.png b/docs/images/integrations-hashicorp-vault-policy-2.png new file mode 100644 index 000000000..719659014 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-policy-2.png differ diff --git a/docs/images/integrations-hashicorp-vault-policy-3.png b/docs/images/integrations-hashicorp-vault-policy-3.png new file mode 100644 index 000000000..76fe2de35 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-policy-3.png differ diff --git a/docs/images/integrations-hashicorp-vault-shell.png b/docs/images/integrations-hashicorp-vault-shell.png new file mode 100644 index 000000000..7d63bde40 Binary files /dev/null and b/docs/images/integrations-hashicorp-vault-shell.png differ diff --git a/docs/images/integrations-hashicorp-vault.png b/docs/images/integrations-hashicorp-vault.png new file mode 100644 index 000000000..e556ffc6b Binary files /dev/null and b/docs/images/integrations-hashicorp-vault.png differ diff --git a/docs/images/integrations.png b/docs/images/integrations.png index fc6593143..368a10c37 100644 Binary files a/docs/images/integrations.png and b/docs/images/integrations.png differ diff --git a/docs/integrations/cloud/checkly.mdx b/docs/integrations/cloud/checkly.mdx new file mode 100644 index 000000000..54b2e7ac5 --- /dev/null +++ b/docs/integrations/cloud/checkly.mdx @@ -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) + + + 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. + + +## 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) \ No newline at end of file diff --git a/docs/integrations/cloud/hashicorp-vault.mdx b/docs/integrations/cloud/hashicorp-vault.mdx new file mode 100644 index 000000000..b757d7456 --- /dev/null +++ b/docs/integrations/cloud/hashicorp-vault.mdx @@ -0,0 +1,159 @@ +--- +title: "HashiCorp Vault" +description: "How to sync secrets from Infisical to HashiCorp Vault" +--- + + + 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. + + +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" ] +} +``` + + + `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. + + +![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) + + + diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 69b7e9a97..79db492ea 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -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 | diff --git a/docs/mint.json b/docs/mint.json index 76925944a..553db5172 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -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", diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 5efc33e27..e873f1a72 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -17,7 +17,8 @@ const integrationSlugNameMapping: Mapping = { 'circleci': 'CircleCI', 'travisci': 'TravisCI', 'supabase': 'Supabase', - 'checkly': 'Checkly' + 'checkly': 'Checkly', + 'hashicorp-vault': 'Vault' } const envMapping: Mapping = { diff --git a/frontend/public/images/integrations/Vault.png b/frontend/public/images/integrations/Vault.png new file mode 100644 index 000000000..3cba8d11d Binary files /dev/null and b/frontend/public/images/integrations/Vault.png differ diff --git a/frontend/src/components/integrations/Integration.tsx b/frontend/src/components/integrations/Integration.tsx index d081e1a9a..6a657c1a0 100644 --- a/frontend/src/components/integrations/Integration.tsx +++ b/frontend/src/components/integrations/Integration.tsx @@ -213,6 +213,8 @@ const IntegrationTile = ({ }; if (!integrationApp && integration.integration !== "checkly") return
; + + const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp; return (
@@ -245,13 +247,15 @@ const IntegrationTile = ({
APP
- {integrationApp ?
+ app.name) : null} - isSelected={integrationApp} + isSelected={isSelected} onChange={(app) => { setIntegrationApp(app); }} - />
:
-
} + /> +
:
-
}
{renderIntegrationSpecificParams(integration)} diff --git a/frontend/src/pages/api/integrations/saveIntegrationAccessToken.ts b/frontend/src/pages/api/integrations/saveIntegrationAccessToken.ts index 0c226ddd3..831c5471e 100644 --- a/frontend/src/pages/api/integrations/saveIntegrationAccessToken.ts +++ b/frontend/src/pages/api/integrations/saveIntegrationAccessToken.ts @@ -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) { diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx index 3cbbbfeb3..435c904f5 100644 --- a/frontend/src/pages/integrations/[id].tsx +++ b/frontend/src/pages/integrations/[id].tsx @@ -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; } diff --git a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx index 9fc1e4a71..8fdb57ec1 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx @@ -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} > - setAccessKey(e.target.value)} /> + setAccessKey(e.target.value)} + /> { + 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 ( +
+ + Vault Integration + + setVaultURL(e.target.value)} /> + + + setVaultNamespace(e.target.value)} + /> + + + setVaultRoleID(e.target.value)} /> + + + setVaultSecretID(e.target.value)} + /> + + + +
+ ); +} + +HashiCorpVaultAuthorizeIntegrationPage.requireAuth = true; \ No newline at end of file diff --git a/frontend/src/pages/integrations/hashicorp-vault/create.tsx b/frontend/src/pages/integrations/hashicorp-vault/create.tsx new file mode 100644 index 000000000..40f2139a0 --- /dev/null +++ b/frontend/src/pages/integrations/hashicorp-vault/create.tsx @@ -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 ? ( +
+ + Vault Integration + + + + + setVaultEnginePath(e.target.value)} + /> + + + setVaultSecretPath(e.target.value)} + /> + + + +
+ ) : ( +
+ ); +} + +HashiCorpVaultCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/railway/authorize.tsx b/frontend/src/pages/integrations/railway/authorize.tsx index 7c983c4e1..a0068bb4b 100644 --- a/frontend/src/pages/integrations/railway/authorize.tsx +++ b/frontend/src/pages/integrations/railway/authorize.tsx @@ -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); diff --git a/frontend/src/pages/integrations/render/authorize.tsx b/frontend/src/pages/integrations/render/authorize.tsx index 8788b5a94..af7535942 100644 --- a/frontend/src/pages/integrations/render/authorize.tsx +++ b/frontend/src/pages/integrations/render/authorize.tsx @@ -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); diff --git a/frontend/src/pages/integrations/supabase/authorize.tsx b/frontend/src/pages/integrations/supabase/authorize.tsx index 379400ae4..f3ff58528 100644 --- a/frontend/src/pages/integrations/supabase/authorize.tsx +++ b/frontend/src/pages/integrations/supabase/authorize.tsx @@ -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); diff --git a/frontend/src/pages/integrations/travisci/authorize.tsx b/frontend/src/pages/integrations/travisci/authorize.tsx index 18107e383..95e50d0f3 100644 --- a/frontend/src/pages/integrations/travisci/authorize.tsx +++ b/frontend/src/pages/integrations/travisci/authorize.tsx @@ -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);