From 37d2d580f49b16bb6012876df6d32b231f547e51 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 13 Jun 2023 10:02:10 +0100 Subject: [PATCH 01/30] Improve API docs for non-E2EE --- backend/src/routes/v3/secrets.ts | 1 - .../api-reference/overview/authentication.mdx | 37 ++- .../overview/encryption-modes/es-mode.mdx | 92 ------- .../overview/encryption-modes/overview.mdx | 57 ----- .../overview/examples/create-secret.mdx | 233 ------------------ .../overview/examples/delete-secret.mdx | 94 ------- .../overview/examples/e2ee-disabled.mdx | 176 +++++++++++++ .../e2ee-enabled.mdx} | 21 +- docs/api-reference/overview/examples/note.mdx | 54 ++++ .../overview/examples/retrieve-secret.mdx | 180 -------------- .../overview/examples/retrieve-secrets.mdx | 195 --------------- .../overview/examples/update-secret.mdx | 229 ----------------- docs/mint.json | 6 +- 13 files changed, 259 insertions(+), 1116 deletions(-) delete mode 100644 docs/api-reference/overview/encryption-modes/es-mode.mdx delete mode 100644 docs/api-reference/overview/encryption-modes/overview.mdx delete mode 100644 docs/api-reference/overview/examples/create-secret.mdx delete mode 100644 docs/api-reference/overview/examples/delete-secret.mdx create mode 100644 docs/api-reference/overview/examples/e2ee-disabled.mdx rename docs/api-reference/overview/{encryption-modes/e2ee-mode.mdx => examples/e2ee-enabled.mdx} (97%) create mode 100644 docs/api-reference/overview/examples/note.mdx delete mode 100644 docs/api-reference/overview/examples/retrieve-secret.mdx delete mode 100644 docs/api-reference/overview/examples/retrieve-secrets.mdx delete mode 100644 docs/api-reference/overview/examples/update-secret.mdx diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 1f3e239be6..56573dd9f3 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -23,7 +23,6 @@ import { router.get( "/raw", query("workspaceId").exists().isString().trim(), - query("workspaceId").exists().isString().trim(), query("environment").exists().isString().trim(), query("secretPath").default("/").isString().trim(), validateRequest, diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index 8bad0114f3..5fa4fbc30e 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -3,32 +3,29 @@ title: "Authentication" description: "How to authenticate with the Infisical Public API" --- -## Essentials +The Public API accepts multiple modes of authentication being via [Infisical Token](/documentation/platform/token) or API Key. -The Public API accepts multiple modes of authentication being via API Key or [Infisical Token](/documentation/platform/token). - -- API Key: Provides full access to all endpoints representing the user without ability to encrypt/decrypt secrets in **E2EE** mode. - [Infisical Token](/documentation/platform/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment. +- API Key: Provides full access to all endpoints representing the user without ability to encrypt/decrypt secrets for **E2EE** endpoints. - - -The API key mode uses an API key to authenticate with the API. + + + The Infisical Token mode uses an Infisical Token to authenticate with the API. -To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. + To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer `. -You can obtain an API key in User Settings > API Keys + You can obtain an Infisical Token in Project Settings > Service Tokens. -![API key dashboard](../../images/api-key-dashboard.png) -![API key in personal settings](../../images/api-key-settings.png) - - + ![token add](../../images/project-token-add.png) + + + The API key mode uses an API key to authenticate with the API. -The Infisical Token mode uses an Infisical Token to authenticate with the API. + To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. -To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer `. + You can obtain an API key in User Settings > API Keys -You can obtain an Infisical Token in Project Settings > Service Tokens. - -![token add](../../images/project-token-add.png) - - \ No newline at end of file + ![API key dashboard](../../images/api-key-dashboard.png) + ![API key in personal settings](../../images/api-key-settings.png) + + \ No newline at end of file diff --git a/docs/api-reference/overview/encryption-modes/es-mode.mdx b/docs/api-reference/overview/encryption-modes/es-mode.mdx deleted file mode 100644 index 2756980ac9..0000000000 --- a/docs/api-reference/overview/encryption-modes/es-mode.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: "ES Mode" ---- - -Encrypted Standard (ES) mode is the easiest way to use Infisical's API. With it, you can make HTTP calls to Infisical -to read/write secrets in plaintext. - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled. -- [Ensure that your project is blind-indexed](../blind-indices). - -Below, we showcase how to execute common CRUD operations to manage secrets in **ES** mode: - - - - - - ```bash - curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw?environment=dev&workspaceId=xxx' \ - --header 'Authorization: Bearer st.xxx' - - ``` - - - - - - - ```bash - curl --location --request POST 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \ - --header 'Authorization: Bearer st.xxx' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "xxx", - "environment": "dev", - "type": "shared", - "secretValue": "SECRET_VALUE", - "secretPath": "/" - }' - ``` - - - - - - - ```bash - curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME?workspaceId=xxx&environment=dev&secretPath=/' \ - --header 'Authorization: Bearer st.xxx' - ``` - - - - - - - ```bash - curl --location --request PATCH 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \ - --header 'Authorization: Bearer st.xxx' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "xxx", - "environment": "dev", - "type": "shared", - "secretValue": "SECRET_VALUE", - "secretPath": "/" - }' - ``` - - - - - - - ```bash - curl --location --request DELETE 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \ - --header 'Authorization: Bearer st.xxx' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "xxx", - "environment": "dev", - "type": "shared", - "secretValue": "SECRET_VALUE", - "secretPath": "/" - }' - ``` - - - - diff --git a/docs/api-reference/overview/encryption-modes/overview.mdx b/docs/api-reference/overview/encryption-modes/overview.mdx deleted file mode 100644 index 84606d1149..0000000000 --- a/docs/api-reference/overview/encryption-modes/overview.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: "Preface" ---- - -Each project in Infisical can be used either in **End-to-End Encrypted (E2EE)** mode or **Encrypted Standard (ES)** mode which dictates how it can be interacted with via the Infisical API. - - - - Secret operations without client-side encryption/decryption - - - Secret operations with client-side encryption/decryption - - - -By default, all projects are initialized in **E2EE** mode which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side. However, this has limitations around functionality and ease-of-use: - -- You cannot make HTTP calls to Infisical to read/write secrets in plaintext. -- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation. - -For this reason, Infisical also provides the **ES** mode of operation to unlock the above limitations by enabling the server to decrypt your values. You can optionally switch a project to using **ES** mode -in your Project Settings. - - - Make no mistake, the limitations of **E2EE** mode do not prevent you from syncing secrets from Infisical to platforms like GitLab. They just imply - that you have to do things the "E2EE-way" such as by embedding the Infisical CLI into your GitLab CI/CD pipelines to fetch and decrypt - secrets on the client-side. - - -## FAQ - - - - We recommend starting with **E2EE** mode and switching to **ES** mode when: - - - Your team needs more power out of non-E2EE features available in **ES** mode such as secret rotation, dynamic secrets, etc. - - Your team wants an easier way to read/write secrets with Infisical. - - - - By default, all projects in Infisical are initialized to **E2EE** mode and can be switched to **ES** mode in the Project Settings by disabling end-to-end encryption. - - - **ES** mode is secure and in fact what most vendors in the secret management industry are doing at the moment. In this mode, secrets are encrypted at rest by - a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server. - - If you're concerned about Infisical Cloud's ability to read your secrets if using **ES** mode in Infisical Cloud, then you may wish to - use Infisical Cloud in **E2EE** mode or self-host Infisical on your own infrastructure and then use **ES** mode; this of course which means setting up firewalls and securing the instance yourself. - - As an organization, we prohibit reading any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization. - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/create-secret.mdx b/docs/api-reference/overview/examples/create-secret.mdx deleted file mode 100644 index 4ea787ff02..0000000000 --- a/docs/api-reference/overview/examples/create-secret.mdx +++ /dev/null @@ -1,233 +0,0 @@ ---- -title: "Create secret" -description: "How to add a secret using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Flow - -1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. -2. Decrypt the (encrypted) project key with the key from your Infisical Token. -3. Encrypt your secret with the project key -4. [Send (encrypted) secret to Infisical](/api-reference/endpoints/secrets/create) - -## Example - - - -```js -const crypto = require('crypto'); -const axios = require('axios'); -const nacl = require('tweetnacl'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const createSecrets = async () => { - const serviceToken = ''; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared'; // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'some_value'; - const secretComment = 'some_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) secret to Infisical - await axios.post( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -createSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def create_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared or "personal" - secret_key = "some_key" - secret_value = "some_value" - secret_comment = "some_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) secret to Infisical - requests.post( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -create_secrets() - -``` - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/delete-secret.mdx b/docs/api-reference/overview/examples/delete-secret.mdx deleted file mode 100644 index ff2ec687cb..0000000000 --- a/docs/api-reference/overview/examples/delete-secret.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: "Delete secret" -description: "How to delete a secret using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create either an [API Key](/api-reference/overview/authentication) or [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Example - - - -```js -const axios = require('axios'); -const BASE_URL = 'https://app.infisical.com'; - -const deleteSecrets = async () => { - const serviceToken = 'your_service_token'; - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key' - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Delete secret from Infisical - await axios.delete( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - }, - } - ); -}; - -deleteSecrets(); -``` - - - -```Python -import requests - -BASE_URL = "https://app.infisical.com" - - -def delete_secrets(): - service_token = "" - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Delete secret from Infisical - requests.delete( - f"{BASE_URL}/api/v2/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -delete_secrets() - -``` - - - - If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header. - - diff --git a/docs/api-reference/overview/examples/e2ee-disabled.mdx b/docs/api-reference/overview/examples/e2ee-disabled.mdx new file mode 100644 index 0000000000..9864a1ff5d --- /dev/null +++ b/docs/api-reference/overview/examples/e2ee-disabled.mdx @@ -0,0 +1,176 @@ +--- +title: "E2EE Disabled" +--- + +Using Infisical's API to read/write secrets with E2EE disabled allows you to create, update, and retrieve secrets +in plaintext. Effectively, this means each such secret operation only requires 1 HTTP call. + + + + Retrieve all secrets for an Infisical project and environment. + + + + ```bash + curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw?environment=environment&workspaceId=workspaceId' \ + --header 'Authorization: Bearer serviceToken' + + ``` + + + + + The ID of the workspace + + + The environment slug + + + Path to secrets in workspace + + + + Create a secret in Infisical. + + + + ```bash + curl --location --request POST 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ + --header 'Authorization: Bearer serviceToken' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "workspaceId": "workspaceId", + "environment": "environment", + "type": "shared", + "secretValue": "secretValue", + "secretPath": "/" + }' + ``` + + + + + Name of secret to create + + + The ID of the workspace + + + The environment slug + + + Value of secret + + + Comment of secret + + + Path to secret in workspace + + + The type of the secret. Valid options are “shared” or “personal” + + + + Retrieve a secret from Infisical. + + + + ```bash + curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \ + --header 'Authorization: Bearer serviceToken' + ``` + + + + + Name of secret to retrieve + + + The ID of the workspace + + + The environment slug + + + Path to secrets in workspace + + + The type of the secret. Valid options are “shared” or “personal” + + + + Update an existing secret in Infisical. + + + + ```bash + curl --location --request PATCH 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ + --header 'Authorization: Bearer serviceToken' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "workspaceId": "workspaceId", + "environment": "environment", + "type": "shared", + "secretValue": "secretValue", + "secretPath": "/" + }' + ``` + + + + + Name of secret to update + + + The ID of the workspace + + + The environment slug + + + Value of secret + + + Path to secret in workspace. + + + The type of the secret. Valid options are “shared” or “personal” + + + + Delete a secret in Infisical. + + + + ```bash + curl --location --request DELETE 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ + --header 'Authorization: Bearer serviceToken' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "workspaceId": "workspaceId", + "environment": "environment", + "type": "shared", + "secretPath": "/" + }' + ``` + + + + + Name of secret to update + + + The ID of the workspace + + + The environment slug + + + Path to secret in workspace. + + + The type of the secret. Valid options are “shared” or “personal” + + + \ No newline at end of file diff --git a/docs/api-reference/overview/encryption-modes/e2ee-mode.mdx b/docs/api-reference/overview/examples/e2ee-enabled.mdx similarity index 97% rename from docs/api-reference/overview/encryption-modes/e2ee-mode.mdx rename to docs/api-reference/overview/examples/e2ee-enabled.mdx index b24ec9fb7c..0cbeaabcf0 100644 --- a/docs/api-reference/overview/encryption-modes/e2ee-mode.mdx +++ b/docs/api-reference/overview/examples/e2ee-enabled.mdx @@ -1,23 +1,16 @@ --- -title: "E2EE Mode" +title: "E2EE Enabled" --- -End-to-End Encrypted (E2EE) mode is the default way to use Infisical's API. With it, you must perform client-side encryption/decryption -when reading/writing secrets via HTTP call to Infisical. - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -Below, we showcase how to execute common CRUD operations to manage secrets in **E2EE** mode: +Using Infisical's API to read/write secrets with E2EE enabled allows you to create, update, and retrieve secrets +but requires you to perform client-side encryption/decryption operations. For this reason, we recommend using one of the available +SDKs instead. + Retrieve all secrets for an Infisical project and environment. ```js const crypto = require('crypto'); const axios = require('axios'); @@ -194,6 +187,7 @@ get_secrets() +Create a secret in Infisical. ```js const crypto = require('crypto'); const axios = require('axios'); @@ -408,6 +402,7 @@ create_secrets() + Retrieve a secret from Infisical. ```js const crypto = require('crypto'); const axios = require('axios'); @@ -569,6 +564,7 @@ get_secret() +Update an existing secret in Infisical. ```js const crypto = require('crypto'); const axios = require('axios'); @@ -779,6 +775,7 @@ update_secret() + Delete a secret in Infisical. ```js const axios = require('axios'); const BASE_URL = 'https://app.infisical.com'; diff --git a/docs/api-reference/overview/examples/note.mdx b/docs/api-reference/overview/examples/note.mdx new file mode 100644 index 0000000000..8491dfaaec --- /dev/null +++ b/docs/api-reference/overview/examples/note.mdx @@ -0,0 +1,54 @@ +--- +title: "Note on E2EE" +--- + +Each project in Infisical can have **End-to-End Encryption (E2EE)** enabled or disabled. + +By default, all projects have **E2EE** enabled which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side; this can be (optionally) disabled. However, this has limitations around functionality and ease-of-use: + +- You cannot make HTTP calls to Infisical to read/write secrets in plaintext. +- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation. + + + + Example read/write secrets without client-side encryption/decryption + + + Example read/write secrets with client-side encryption/decryption + + + +## FAQ + + + + We recommend starting with having **E2EE** enabled and disabling it if: + + - You're self-hosting Infisical, so having your instance of Infisical be able to read your secrets isn't an issue. + - You want an easier way to read/write secrets with Infisical. + - You need more power out of non-E2EE features such as secret rotation, dynamic secrets, etc. + + + + You can enable/disable E2EE for your project in Infisical in the Project Settings. + + + It is secure and in fact how most vendors in our industry are able to offer features like secret rotation. In this mode, secrets are encrypted at rest by + a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server. + + If you're concerned about Infisical Cloud's ability to read your secrets, then you may wish to + use it with **E2EE** enabled or self-host Infisical on your own infrastructure and disable E2EE there. + + As an organization, we do not read any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization. + + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/retrieve-secret.mdx b/docs/api-reference/overview/examples/retrieve-secret.mdx deleted file mode 100644 index a65876253e..0000000000 --- a/docs/api-reference/overview/examples/retrieve-secret.mdx +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: "Retrieve secret" -description: "How to get a secret using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Flow - -1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. -2. [Get the secret from your project and environment](/api-reference/endpoints/secrets/read-one). -3. Decrypt the (encrypted) project key with the key from your Infisical Token. -4. Decrypt the (encrypted) secret - -## Example - - - -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecret = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get the secret from your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace, - type: secretType // optional, defaults to 'shared' - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecret = data.secret; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secret value - - const secretValue = decrypt({ - ciphertext: encryptedSecret.secretValueCiphertext, - iv: encryptedSecret.secretValueIV, - tag: encryptedSecret.secretValueTag, - secret: projectKey - }); - - console.log('secret: ', ({ - secretKey, - secretValue - })); -} - -getSecret(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secret from your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - "type": secret_type # optional, defaults to "shared" - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secret = data["secret"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secret value - secret_value = decrypt( - ciphertext=encrypted_secret["secretValueCiphertext"], - iv=encrypted_secret["secretValueIV"], - tag=encrypted_secret["secretValueTag"], - secret=project_key, - ) - - print("secret: ", { - "secret_key": secret_key, - "secret_value": secret_value - }) - - -get_secret() - -``` - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/retrieve-secrets.mdx b/docs/api-reference/overview/examples/retrieve-secrets.mdx deleted file mode 100644 index f237822aad..0000000000 --- a/docs/api-reference/overview/examples/retrieve-secrets.mdx +++ /dev/null @@ -1,195 +0,0 @@ ---- -title: "Retrieve secrets" -description: "How to get all secrets using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Flow - -1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. -2. [Get secrets for your project and environment](/api-reference/endpoints/secrets/read). -3. Decrypt the (encrypted) project key with the key from your Infisical Token. -4. Decrypt the (encrypted) secrets - -## Example - - - -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get secrets for your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecrets = data.secrets; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secrets - const secrets = encryptedSecrets.map((secret) => { - const secretKey = decrypt({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - secret: projectKey - }); - - const secretValue = decrypt({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - secret: projectKey - }); - - return ({ - secretKey, - secretValue - }); - }); - - console.log('secrets: ', secrets); -} - -getSecrets(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secrets for your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secrets = data["secrets"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secrets - secrets = [] - for secret in encrypted_secrets: - secret_key = decrypt( - ciphertext=secret["secretKeyCiphertext"], - iv=secret["secretKeyIV"], - tag=secret["secretKeyTag"], - secret=project_key, - ) - - secret_value = decrypt( - ciphertext=secret["secretValueCiphertext"], - iv=secret["secretValueIV"], - tag=secret["secretValueTag"], - secret=project_key, - ) - - secrets.append( - { - "secret_key": secret_key, - "secret_value": secret_value, - } - ) - - print("secrets:", secrets) - - -get_secrets() - -``` - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/update-secret.mdx b/docs/api-reference/overview/examples/update-secret.mdx deleted file mode 100644 index 49255aa79b..0000000000 --- a/docs/api-reference/overview/examples/update-secret.mdx +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: "Update secret" -description: "How to update a secret using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Flow - -1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. -2. Decrypt the (encrypted) project key with the key from your Infisical Token. -3. Encrypt your updated secret with the project key -4. [Send (encrypted) updated secret to Infical](/api-reference/endpoints/secrets/update) - -## Example - - - -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const updateSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'updated_value'; - const secretComment = 'updated_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your updated secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) updated secret to Infisical - await axios.patch( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -updateSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def update_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - secret_value = "updated_value" - secret_comment = "updated_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your updated secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) updated secret to Infisical - requests.patch( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -update_secret() - -``` - - \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 7be9707706..9dba735e61 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -250,9 +250,9 @@ { "group": "Examples", "pages": [ - "api-reference/overview/encryption-modes/overview", - "api-reference/overview/encryption-modes/es-mode", - "api-reference/overview/encryption-modes/e2ee-mode" + "api-reference/overview/examples/note", + "api-reference/overview/examples/e2ee-disabled", + "api-reference/overview/examples/e2ee-enabled" ] }, "api-reference/overview/blind-indices" From e24c1f38e068a349565c6e7596700e81e0ea871c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 13 Jun 2023 11:23:13 +0100 Subject: [PATCH 02/30] Add REST API integration option in docs introduction --- docs/documentation/getting-started/api.mdx | 59 +++++++++++++++++++ .../getting-started/introduction.mdx | 8 +++ docs/mint.json | 3 +- 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 docs/documentation/getting-started/api.mdx diff --git a/docs/documentation/getting-started/api.mdx b/docs/documentation/getting-started/api.mdx new file mode 100644 index 0000000000..7bcb2d76a9 --- /dev/null +++ b/docs/documentation/getting-started/api.mdx @@ -0,0 +1,59 @@ +--- +title: "REST API" +--- + +Infisical's Public (REST) API is the most flexible, platform-agnostic way to read/write secrets for your application. + +Prerequisites: + +- Have a project with secrets ready in [Infisical Cloud](https://app.infisical.com). +- Create an [Infisical Token](/documentation/platform/token) scoped to an environment in your project in Infisical. + +To keep it simple, we're going to fetch secrets from the API with **End-to-End Encryption (E2EE)** disabled. + + + It's possible to use the API with **E2EE** enabled but this means learning about how encryption works with Infisical and performing client-side encryption/decryption operations yourself. + yourself. + + If **E2EE** is a must for your team, we recommend either using one of the [Infisical SDKs](/documentation/getting-started/sdks) or checking out the [examples for E2EE](/api-reference/overview/examples/e2ee-enabled). + + +## Configuration + +Head to your Project Settings, where you created your service token, and un-check the **E2EE** setting. + +## Retrieve Secret + +Retrieve a secret from the project and environment in Infisical scoped to your service token by making a HTTP request with the following format/details: + +```bash +curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \ + --header 'Authorization: Bearer serviceToken' +``` + + + Name of secret to retrieve + + + The ID of the workspace + + + The environment slug + + + Path to secrets in workspace + + + The type of the secret. Valid options are “shared” or “personal” + + +Depending on your application requirements, you may wish to use Infisical's API in different ways such as by retaining **E2EE** +or fetching multiple secrets at once instead of one at a time. + +Whatever the case, we recommend glossing over the [API Examples](/api-reference/overview/examples/note) +to gain a deeper understanding of how you to best leverage the Infisical API for your use-case. + +See also: + +- Explore the [API Examples](/api-reference/overview/examples/note) +- [API Reference](/api-reference/overview/introduction) \ No newline at end of file diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index 2db9f52841..46a2f48e30 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -42,6 +42,14 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical > Fetch and save secrets as native Kubernetes secrets + + Fetch secrets via HTTP request + ## Resources diff --git a/docs/mint.json b/docs/mint.json index 9dba735e61..b4412dfe29 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -92,7 +92,8 @@ "documentation/getting-started/sdks", "documentation/getting-started/cli", "documentation/getting-started/docker", - "documentation/getting-started/kubernetes" + "documentation/getting-started/kubernetes", + "documentation/getting-started/api" ] }, { From f4404f66b83f9737a22e9f18acd432ba5d1d278e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 13 Jun 2023 11:30:47 +0100 Subject: [PATCH 03/30] Correct link to E2EE API usage example --- docs/documentation/getting-started/api.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/getting-started/api.mdx b/docs/documentation/getting-started/api.mdx index 7bcb2d76a9..2799829116 100644 --- a/docs/documentation/getting-started/api.mdx +++ b/docs/documentation/getting-started/api.mdx @@ -15,7 +15,7 @@ To keep it simple, we're going to fetch secrets from the API with **End-to-End E It's possible to use the API with **E2EE** enabled but this means learning about how encryption works with Infisical and performing client-side encryption/decryption operations yourself. yourself. - If **E2EE** is a must for your team, we recommend either using one of the [Infisical SDKs](/documentation/getting-started/sdks) or checking out the [examples for E2EE](/api-reference/overview/examples/e2ee-enabled). + If **E2EE** is a must for your team, we recommend either using one of the [Infisical SDKs](/documentation/getting-started/sdks) or checking out the [examples for E2EE](/api-reference/overview/examples/e2ee-disabled). ## Configuration From d590dd5db8011e148df43a5fa2a20f107c3ca71d Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Tue, 13 Jun 2023 19:24:57 +0530 Subject: [PATCH 04/30] feat(folder-sec-overview): added folder path support in get secrets and get folders --- .../controllers/v1/secretsFolderController.ts | 27 +++++++++++++++---- .../src/controllers/v2/secretsController.ts | 13 ++++++--- backend/src/routes/v1/secretsFolder.ts | 1 + 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 06d68a41df..c1ce7ffc44 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -11,6 +11,7 @@ import { validateFolderName, generateFolderId, getParentFromFolderId, + getFolderByPath, } from "../../services/FolderService"; import { ADMIN, MEMBER } from "../../variables"; import { validateMembership } from "../../helpers/membership"; @@ -177,11 +178,13 @@ export const deleteFolder = async (req: Request, res: Response) => { // TODO: validate workspace export const getFolders = async (req: Request, res: Response) => { - const { workspaceId, environment, parentFolderId } = req.query as { - workspaceId: string; - environment: string; - parentFolderId?: string; - }; + const { workspaceId, environment, parentFolderId, parentFolderPath } = + req.query as { + workspaceId: string; + environment: string; + parentFolderId?: string; + parentFolderPath?: string; + }; const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (!folders) { @@ -196,6 +199,20 @@ export const getFolders = async (req: Request, res: Response) => { acceptedRoles: [ADMIN, MEMBER], }); + // if instead of parentFolderId given a path like /folder1/folder2 + if (parentFolderPath) { + const folder = getFolderByPath(folders.nodes, parentFolderPath); + if (!folder) { + res.send({ folders: [], dir: [] }); + return; + } + // dir is not needed at present as this is only used in overview section of secrets + res.send({ + folders: folder.children.map(({ id, name }) => ({ id, name })), + dir: [{ name: folder.name, id: folder.id }], + }); + } + if (!parentFolderId) { const rootFolders = folders.nodes.children.map(({ id, name }) => ({ id, diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index ae1de8cfed..256ad248c2 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -700,11 +700,15 @@ export const getSecrets = async (req: Request, res: Response) => { (!folders && folderId && folderId !== "root") || (!folders && secretPath) ) { - throw BadRequestError({ message: "Folder not found" }); + res.send({ secrets: [] }); + return; } if (folders && folderId !== "root") { const folder = searchByFolderId(folders.nodes, folderId as string); - if (!folder) throw BadRequestError({ message: "Folder not found" }); + if (!folder) { + res.send({ secrets: [] }); + return; + } } if (req.authData.authPayload instanceof ServiceTokenData) { @@ -720,10 +724,11 @@ export const getSecrets = async (req: Request, res: Response) => { } if (folders && secretPath) { - if (!folders) throw BadRequestError({ message: "Folder not found" }); + // avoid throwing error and send empty list const folder = getFolderByPath(folders.nodes, secretPath as string); if (!folder) { - throw BadRequestError({ message: "Secret path not found" }); + res.send({ secrets: [] }); + return; } folderId = folder.id; } diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index 2644a835be..83517f1ab0 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -63,6 +63,7 @@ router.get( query("workspaceId").exists().isString().trim(), query("environment").exists().isString().trim(), query("parentFolderId").optional().isString().trim(), + query("parentFolderPath").optional().isString().trim(), validateRequest, getFolders ); From a6cf7107b9ea487e970ea208fc843565ddf26ac6 Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Tue, 13 Jun 2023 19:25:50 +0530 Subject: [PATCH 05/30] feat(folder-sec-overview): implemented folder based ui for sec overview --- .../src/hooks/api/secretFolders/index.tsx | 8 +- .../src/hooks/api/secretFolders/queries.tsx | 56 ++- frontend/src/hooks/api/secretFolders/types.ts | 6 + frontend/src/hooks/api/secrets/queries.tsx | 31 +- frontend/src/hooks/api/secrets/types.ts | 1 + frontend/src/pages/dashboard/[id].tsx | 13 +- frontend/src/styles/globals.css | 32 +- .../DashboardPage/DashboardEnvOverview.tsx | 411 ++++++++++-------- .../EnvComparisonRow/EnvComparisonRow.tsx | 10 +- .../EnvComparisonRow/FolderComparisonRow.tsx | 42 ++ 10 files changed, 392 insertions(+), 218 deletions(-) create mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx diff --git a/frontend/src/hooks/api/secretFolders/index.tsx b/frontend/src/hooks/api/secretFolders/index.tsx index c1cc056d1b..8b48f3e204 100644 --- a/frontend/src/hooks/api/secretFolders/index.tsx +++ b/frontend/src/hooks/api/secretFolders/index.tsx @@ -1 +1,7 @@ -export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries'; +export { + useCreateFolder, + useDeleteFolder, + useGetProjectFolders, + useGetProjectFoldersBatch, + useUpdateFolder +} from './queries'; diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index 008d6f26f5..57b58afea4 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiRequest } from '@app/config/request'; @@ -7,6 +7,7 @@ import { secretSnapshotKeys } from '../secretSnapshots/queries'; import { CreateFolderDTO, DeleteFolderDTO, + GetProjectFoldersBatchDTO, GetProjectFoldersDTO, TSecretFolder, UpdateFolderDTO @@ -17,6 +18,26 @@ const queryKeys = { ['secret-folders', { workspaceId, environment, parentFolderId }] as const }; +const fetchProjectFolders = async ( + workspaceId: string, + environment: string, + parentFolderId?: string, + parentFolderPath?: string +) => { + const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>( + '/api/v1/folders', + { + params: { + workspaceId, + environment, + parentFolderId, + parentFolderPath + } + } + ); + return data; +}; + export const useGetProjectFolders = ({ workspaceId, parentFolderId, @@ -27,19 +48,7 @@ export const useGetProjectFolders = ({ useQuery({ queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId), enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused, - queryFn: async () => { - const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>( - '/api/v1/folders', - { - params: { - workspaceId, - environment, - parentFolderId - } - } - ); - return data; - }, + queryFn: async () => fetchProjectFolders(workspaceId, environment, parentFolderId), select: useCallback( ({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({ dir, @@ -53,6 +62,25 @@ export const useGetProjectFolders = ({ ) }); +export const useGetProjectFoldersBatch = ({ + folders = [], + isPaused, + parentFolderPath +}: GetProjectFoldersBatchDTO) => + useQueries({ + queries: folders.map(({ workspaceId, environment, parentFolderId }) => ({ + queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderPath), + queryFn: async () => + fetchProjectFolders(workspaceId, environment, parentFolderId, parentFolderPath), + enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused, + select: (data: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({ + environment, + folders: data.folders, + dir: data.dir + }) + })) + }); + export const useCreateFolder = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index a9ca176411..44a6a0859d 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -11,6 +11,12 @@ export type GetProjectFoldersDTO = { sortDir?: 'asc' | 'desc'; }; +export type GetProjectFoldersBatchDTO = { + folders: Omit[]; + isPaused?: boolean; + parentFolderPath?: string; +}; + export type CreateFolderDTO = { workspaceId: string; environment: string; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index e4ad89dd6f..89137b03c4 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign */ +import { useCallback } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { @@ -29,14 +30,16 @@ export const secretKeys = { const fetchProjectEncryptedSecrets = async ( workspaceId: string, env: string | string[], - folderId?: string + folderId?: string, + secretPath?: string ) => { if (typeof env === 'string') { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { params: { environment: env, workspaceId, - folderId: folderId || undefined + folderId: folderId || undefined, + secretPath } }); return data.secrets; @@ -52,7 +55,8 @@ const fetchProjectEncryptedSecrets = async ( params: { environment: envPoint, workspaceId, - folderId + folderId, + secretPath } }); allEnvData = allEnvData.concat(data.secrets); @@ -77,7 +81,7 @@ export const useGetProjectSecrets = ({ enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId), - select: (data) => { + select: useCallback((data: EncryptedSecret[]) => { const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = decryptFileKey; const key = decryptAssymmetric({ @@ -146,21 +150,24 @@ export const useGetProjectSecrets = ({ } }); return { secrets: sharedSecrets }; - } + }, []) }); export const useGetProjectSecretsByKey = ({ workspaceId, env, decryptFileKey, - isPaused + isPaused, + folderId, + secretPath }: GetProjectSecretsDTO) => useQuery({ // wait for all values to be available enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, - queryKey: secretKeys.getProjectSecret(workspaceId, env), - queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), - select: (data) => { + // right now secretpath is passed as folderid as only this is used in overview + queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath), + queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), + select: useCallback((data: EncryptedSecret[]) => { const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = decryptFileKey; const key = decryptAssymmetric({ @@ -235,7 +242,7 @@ export const useGetProjectSecretsByKey = ({ }); return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length }; - } + }, []) }); const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { @@ -256,7 +263,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) => enabled: Boolean(dto.secretId && dto.decryptFileKey), queryKey: secretKeys.getSecretVersion(dto.secretId), queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit), - select: (data) => { + select: useCallback((data: EncryptedSecretVersion[]) => { const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = dto.decryptFileKey; const key = decryptAssymmetric({ @@ -278,7 +285,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) => }) })) .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); - } + }, []) }); export const useBatchSecretsOp = () => { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 2a5938c681..5bde496e26 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -96,6 +96,7 @@ export type GetProjectSecretsDTO = { env: string | string[]; decryptFileKey: UserWsKeyPair; folderId?: string; + secretPath?: string; isPaused?: boolean; onSuccess?: (data: DecryptedSecret[]) => void; }; diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index 8d31b42917..3c6fdf9951 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -12,13 +12,6 @@ const Dashboard = () => { const queryEnv = router.query.env as string; const isOverviewMode = !queryEnv; - const onExploreEnv = (slug: string) => { - router.push({ - pathname: router.pathname, - query: { ...router.query, env: slug } - }); - }; - return ( <> @@ -29,11 +22,7 @@ const Dashboard = () => {
- {isOverviewMode ? ( - - ) : ( - - )} + {isOverviewMode ? : }
); diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index feca862f66..693da7ce69 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -4,20 +4,20 @@ @layer utilities { .flex-0 { - flex:0; + flex: 0; } .flex-2 { flex-grow: 2; } .flex-3 { - flex-grow: 3; + flex-grow: 3; } } @layer components { .secret-table { - @apply bg-mineshaft-800 text-left text-bunker-300 w-full; + @apply w-full bg-mineshaft-800 text-left text-bunker-300; } /* padding except for comment column */ @@ -29,13 +29,37 @@ @apply py-1 px-1 pr-2 text-sm; } - .secret-table th:not(:last-child),.secret-table td:not(:last-child) { + .secret-table th:not(:last-child), + .secret-table td:not(:last-child) { @apply border-r border-mineshaft-600; } .secret-table tr { @apply border-b border-mineshaft-600; } + + .breadcrumb::after, + .breadcrumb::before { + content: ''; + height: 60%; + width: 100%; + z-index: -1; + display: block; + position: absolute; + @apply bg-mineshaft-800; + } + + .breadcrumb::after { + left: 5px; + bottom: -3px; + transform: skew(-30deg); + } + + .breadcrumb::before { + left: 5px; + top: -3px; + transform: skew(30deg); + } } @import '@fontsource/inter/400.css'; diff --git a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx index 7427bbc3e2..c95a795db4 100644 --- a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx +++ b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx @@ -1,35 +1,32 @@ import { useEffect, useMemo, useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useRouter } from 'next/router'; -import { faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; +import { faFolderOpen, faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { yupResolver } from '@hookform/resolvers/yup'; import NavHeader from '@app/components/navigation/NavHeader'; import { Button, Input, TableContainer, Tooltip } from '@app/components/v2'; import { useWorkspace } from '@app/context'; import { + useGetProjectFoldersBatch, useGetProjectSecretsByKey, useGetUserWsEnvironments, useGetUserWsKey } from '@app/hooks/api'; -import { WorkspaceEnv } from '@app/hooks/api/types'; import { EnvComparisonRow } from './components/EnvComparisonRow'; -import { FormData, schema } from './DashboardPage.utils'; +import { FolderComparisonRow } from './components/EnvComparisonRow/FolderComparisonRow'; -export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { +export const DashboardEnvOverview = () => { const { t } = useTranslation(); const router = useRouter(); - const [selectedEnv, setSelectedEnv] = useState(null); - const { currentWorkspace, isLoading } = useWorkspace(); const workspaceId = currentWorkspace?._id as string; const { data: latestFileKey } = useGetUserWsKey(workspaceId); const [searchFilter, setSearchFilter] = useState(''); + const secretPath = router.query?.secretPath as string; useEffect(() => { if (!isLoading && !workspaceId && router.isReady) { @@ -38,14 +35,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { }, [isLoading, workspaceId, router.isReady]); const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ - workspaceId, - onSuccess: (data) => { - // get an env with one of the access available - const env = data.find(({ isReadDenied }) => !isReadDenied); - if (env) { - setSelectedEnv(env); - } - } + workspaceId }); const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied); @@ -54,17 +44,32 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { workspaceId, env: userAvailableEnvs?.map((env) => env.slug) ?? [], decryptFileKey: latestFileKey!, - isPaused: false + isPaused: false, + secretPath }); - const method = useForm({ - // why any: well yup inferred ts expects other keys to defined as undefined - defaultValues: secrets as any, - values: secrets as any, - mode: 'onBlur', - resolver: yupResolver(schema) + const folders = useGetProjectFoldersBatch({ + folders: + userAvailableEnvs?.map((env) => ({ + environment: env.slug, + workspaceId + })) ?? [], + parentFolderPath: secretPath }); + const foldersGroupedByEnv = useMemo(() => { + const res: Record> = {}; + folders.forEach(({ data }) => { + data?.folders + ?.filter(({ name }) => name.toLowerCase().includes(searchFilter)) + ?.forEach((folder) => { + if (!res?.[folder.name]) res[folder.name] = {}; + res[folder.name][data.environment] = true; + }); + }); + return res; + }, [folders, userAvailableEnvs, searchFilter]); + const numSecretsMissingPerEnv = useMemo(() => { // first get all sec in the env then subtract with total to get missing ones const secPerEnvMissing: Record = Object.fromEntries( @@ -81,7 +86,43 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { return secPerEnvMissing; }, [secrets, userAvailableEnvs]); - const isReadOnly = selectedEnv?.isWriteDenied; + const onExploreEnv = (slug: string) => { + const query: Record = { ...router.query, env: slug }; + delete query.secretPath; + // the dir return will have the present directory folder id + // use that when clicking on explore to redirect user to there + const envFolder = folders.find(({ data }) => slug === data?.environment); + const dir = envFolder?.data?.dir?.pop(); + if (dir) { + query.folderId = dir.id; + } + + router.push({ + pathname: router.pathname, + query + }); + }; + + const onFolderClick = (path: string) => { + router.push({ + pathname: router.pathname, + query: { + ...router.query, + secretPath: `${router.query?.secretPath || ''}/${path}` + } + }); + }; + + const onFolderCrumbClick = (index: number) => { + const newSecPath = secretPath.split('/').filter(Boolean).slice(0, index).join('/'); + const query = { ...router.query, secretPath: `/${newSecPath}` } as Record; + // root condition + if (index === 0) delete query.secretPath; + router.push({ + pathname: router.pathname, + query + }); + }; if (isSecretsLoading || isEnvListLoading) { return ( @@ -91,165 +132,195 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { ); } + const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) => + secret.toUpperCase().includes(searchFilter.toUpperCase()) + ); // when secrets is not loading and secrets list is empty - const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase()))?.length; + const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length; + const isFoldersEmtpy = + !folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) && + !Object.keys(foldersGroupedByEnv).length; + const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty; return (
- -
- {/* breadcrumb row */} -
- +
+ +
+
+

Secrets Overview

+

+ Inject your secrets using + + Infisical CLI + + or + + Infisical SDKs + +

+
+
+
+
onFolderCrumbClick(0)} + onKeyDown={() => null} + role="button" + tabIndex={0} + > +
-
-

Secrets Overview

-

- Inject your secrets using - ( +

-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
-
-
-
-
{0}
+ {path}
-
-
-
Secret
-
-
- {numSecretsMissingPerEnv && - userAvailableEnvs?.map((env) => { - return ( -
-
- {env.name} - {numSecretsMissingPerEnv[env.slug] > 0 && ( -
- - - {numSecretsMissingPerEnv[env.slug]} - - -
- )} + ))} +
+
+ setSearchFilter(e.target.value)} + leftIcon={} + /> +
+
+
+
+
+
{0}
+
+
+
+
Secret
+
+
+ {numSecretsMissingPerEnv && + userAvailableEnvs?.map((env) => { + return ( +
+
+ {env.name} + {numSecretsMissingPerEnv[env.slug] > 0 && ( +
+ + + {numSecretsMissingPerEnv[env.slug]} + +
-
- ); - })} -
-
- {!isDashboardSecretEmpty && ( - - - - {Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase())).map((key, index) => ( - - ))} - -
-
- )} - {isDashboardSecretEmpty && ( -
-
- - No secrets found. - To add more secrets you can explore any environment. + )}
- )} - {/* In future, we should add an option to add environments here -
- -
*/} -
-
-
-
0
+ ); + })} +
+
+ {!isDashboardEmpty && ( + + + + {Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => ( + + ))} + {Object.keys(secrets?.secrets || {}) + ?.filter((secret: any) => + secret.toUpperCase().includes(searchFilter.toUpperCase()) + ) + .map((key) => ( + + ))} + +
+
+ )} + {isDashboardEmpty && ( +
+
+ + No secrets/folders found. + To add more secrets you can explore any environment.
-
- 0 - -
- {userAvailableEnvs?.map((env) => { - return ( -
- -
- ); - })}
+ )} +
+
+
+
0
- - +
+ 0 + +
+ {userAvailableEnvs?.map((env) => { + return ( +
+ +
+ ); + })} +
+
); }; diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx index cb12a3d9ad..6ef1eb45a0 100644 --- a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx @@ -1,11 +1,10 @@ /* eslint-disable react/jsx-no-useless-fragment */ import { useCallback, useState } from 'react'; -import { faCircle, faEye, faEyeSlash, faMinus } from '@fortawesome/free-solid-svg-icons'; +import { faCircle, faEye, faEyeSlash, faKey, faMinus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { twMerge } from 'tailwind-merge'; type Props = { - index: number; secrets: any[] | undefined; // permission and external state's that decided to hide or show isReadOnly?: boolean; @@ -30,7 +29,7 @@ const DashboardInput = ({ if (val === undefined) return ( - + ); if (val?.length === 0) @@ -110,7 +109,6 @@ const DashboardInput = ({ }; export const EnvComparisonRow = ({ - index, secrets, isSecretValueHidden, isReadOnly, @@ -126,7 +124,9 @@ export const EnvComparisonRow = ({ return ( -
{index + 1}
+
+ +
diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx new file mode 100644 index 0000000000..b63c05dc2c --- /dev/null +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx @@ -0,0 +1,42 @@ +import { faCheck, faFolder, faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +type Props = { + folderInEnv: Record; + userAvailableEnvs?: Array<{ slug: string; name: string }>; + folderName: string; + onClick: (folderName: string) => void; +}; + +export const FolderComparisonRow = ({ + folderInEnv = {}, + userAvailableEnvs = [], + folderName, + onClick +}: Props) => ( + onClick(folderName)} + > + +
+ +
+ + +
{folderName}
+ + {userAvailableEnvs?.map(({ slug }) => ( + + + + ))} + +); From 3d70333f9c348ef3bb1d84677e252ce894f5e702 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 13 Jun 2023 15:31:55 +0100 Subject: [PATCH 06/30] Update password-reset email response --- backend/src/controllers/v1/passwordController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index bdeae0fd46..da1903ebaa 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -36,8 +36,8 @@ export const emailPasswordReset = async (req: Request, res: Response) => { if (!user || !user?.publicKey) { // case: user has already completed account - return res.status(403).send({ - message: "If an account exists with this email, a password reset link has been sent" + return res.status(200).send({ + message:"If an account exists with this email, a password reset link has been sent" }); } From b1b32a34c9982685039237230cb1a1b544a94aa4 Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Tue, 13 Jun 2023 20:16:14 +0530 Subject: [PATCH 07/30] feat(folder-sec-overview): made folder cell fully select --- .../components/EnvComparisonRow/FolderComparisonRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx index b63c05dc2c..a8870afc92 100644 --- a/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx @@ -18,13 +18,13 @@ export const FolderComparisonRow = ({ className="group flex min-w-full cursor-pointer flex-row items-center hover:bg-mineshaft-800" onClick={() => onClick(folderName)} > - +
-
{folderName}
+
{folderName}
{userAvailableEnvs?.map(({ slug }) => ( Date: Tue, 13 Jun 2023 18:28:30 -0400 Subject: [PATCH 08/30] add terraform docs --- docs/integrations/frameworks/terraform.mdx | 97 +++++++++++++++++----- 1 file changed, 77 insertions(+), 20 deletions(-) diff --git a/docs/integrations/frameworks/terraform.mdx b/docs/integrations/frameworks/terraform.mdx index 7161ec586d..0826a833ec 100644 --- a/docs/integrations/frameworks/terraform.mdx +++ b/docs/integrations/frameworks/terraform.mdx @@ -1,34 +1,91 @@ --- title: "Terraform" -description: "How to use Infisical to inject environment variables and secrets into terraform." +description: "Fetch Secrets From Infisical With Terraform" --- -Prerequisites: +This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform. -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -- [Install the CLI](/cli/overview) +## Prerequisites -## Initialize Infisical for your [Terraform](https://www.terraform.io/) project +- Basic understanding of Terraform +- Install [Terraform](https://www.terraform.io/downloads.html) -```bash -# navigate to the root of your of your project -cd /path/to/project +## Steps -# then initialize Infisical -infisical init +### 1. Define Required Providers + +Specify `infisical` in the `required_providers` block within the `terraform` block of your configuration file. If you would like to use a specific version of the provider, uncomment and replace `` with the version of the Infisical provider that you want to use. + +```hcl main.tf +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} ``` -## Run terraform as usual but with Infisical +### 2. Configure the Infisical Provider -```bash -infisical run -- +Set up the Infisical provider by specifying the `host` and `service_token`. Replace `<>` in `service_token` with your actual token. The `host` is only required if you are using a self-hosted instance of Infisical. -# Example -infisical run -- terraform plan +```hcl main.tf +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + service_token = "<>" # Get token https://infisical.com/docs/documentation/platform/token +} ``` - - To inject any arbitrary variable to terraform, you have - to prefix them with `TF_VAR`. Read more about that - [here](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_var_name). - + + It is recommended to use Terraform variables to pass your service token dynamically to avoid hard coding it + + +### 3. Fetch Infisical Secrets + +Use the `infisical_secrets` data source to fetch your secrets. This is defined with an empty block `{}` as the provider automatically fetches all secrets associated with your service token. + +```hcl main.tf +data "infisical_secrets" "my-secrets" {} +``` + +### 4. Define Outputs + +As an example, we are going to output your fetched secrets. Replace `SECRET-NAME` with the actual name of your secret. + +For a single secret: + +```hcl main.tf +output "single-secret" { + value = data.infisical_secrets.my-secrets.secrets["SECRET-NAME"] +} +``` + +For all secrets: + +```hcl +output "all-secrets" { + value = data.infisical_secrets.my-secrets.secrets +} +``` + +### 5. Run Terraform + +Once your configuration is complete, initialize your Terraform working directory: + +```bash +$ terraform init +``` + +Then, run the plan command to view the fetched secrets: + +```bash +$ terraform plan +``` + +Terraform will now fetch your secrets from Infisical and display them as output according to your configuration. + +## Conclusion + +You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs). From 786778fef62698800e1cb6af55dd64bc281989a5 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 13 Jun 2023 21:56:15 -0400 Subject: [PATCH 09/30] isolate gamma environment --- .github/values.yaml | 4 +- .github/workflows/build-staging-img.yml | 149 ++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build-staging-img.yml diff --git a/.github/values.yaml b/.github/values.yaml index 819df5066d..bab85c47e9 100644 --- a/.github/values.yaml +++ b/.github/values.yaml @@ -6,7 +6,7 @@ frontend: secrets.infisical.com/auto-reload: "true" replicaCount: 2 image: - repository: infisical/frontend + repository: infisical/staging_deployment_frontend tag: "latest" pullPolicy: Always kubeSecretRef: managed-secret-frontend @@ -25,7 +25,7 @@ backend: secrets.infisical.com/auto-reload: "true" replicaCount: 2 image: - repository: infisical/backend + repository: infisical/staging_deployment_backend tag: "latest" pullPolicy: Always kubeSecretRef: managed-backend-secret diff --git a/.github/workflows/build-staging-img.yml b/.github/workflows/build-staging-img.yml new file mode 100644 index 0000000000..4f17aae65f --- /dev/null +++ b/.github/workflows/build-staging-img.yml @@ -0,0 +1,149 @@ +name: Build, Publish and Deploy to Gamma +on: + workflow_dispatch: + push: + paths: + - "backend/**" + - "frontend/**" + +jobs: + backend-image: + name: Build backend image + runs-on: ubuntu-latest + steps: + - name: ☁️ Checkout source + uses: actions/checkout@v3 + - name: 📦 Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: 🧪 Run tests + run: npm run test:ci + working-directory: backend + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 📦 Build backend and export to Docker + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + load: true + context: backend + tags: infisical/staging_deployment_backend:test + - name: ⏻ Spawn backend container and dependencies + run: | + docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull + - name: 🧪 Test backend image + run: | + ./.github/resources/healthcheck.sh infisical-backend-test + - name: ⏻ Shut down backend container and dependencies + run: | + docker compose -f .github/resources/docker-compose.be-test.yml down + - name: 🏗️ Build backend and push + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: backend + tags: | + infisical/staging_deployment_backend:${{ steps.commit.outputs.short }} + infisical/staging_deployment_backend:latest + platforms: linux/amd64,linux/arm64 + + frontend-image: + name: Build frontend image + runs-on: ubuntu-latest + steps: + - name: ☁️ Checkout source + uses: actions/checkout@v3 + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 📦 Build frontend and export to Docker + uses: depot/build-push-action@v1 + with: + load: true + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + project: 64mmf0n610 + context: frontend + tags: infisical/staging_deployment_frontend:test + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + - name: ⏻ Spawn frontend container + run: | + docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test + - name: 🧪 Test frontend image + run: | + ./.github/resources/healthcheck.sh infisical-frontend-test + - name: ⏻ Shut down frontend container + run: | + docker stop infisical-frontend-test + - name: 🏗️ Build frontend and push + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + push: true + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + context: frontend + tags: | + infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }} + infisical/staging_deployment_frontend:latest + platforms: linux/amd64,linux/arm64 + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + gamma-deployment: + name: Deploy to gamma + runs-on: ubuntu-latest + needs: [frontend-image, backend-image] + steps: + - name: ☁️ Checkout source + uses: actions/checkout@v3 + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.10.0 + - name: Install infisical helm chart + run: | + helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + helm repo update + - name: Install kubectl + uses: azure/setup-kubectl@v3 + - name: Install doctl + uses: digitalocean/action-doctl@v2 + with: + token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + - name: Save DigitalOcean kubeconfig with short-lived credentials + run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179 + - name: switch to gamma namespace + run: kubectl config set-context --current --namespace=gamma + - name: test kubectl + run: kubectl get ingress + - name: Download helm values to file and upgrade gamma deploy + run: | + wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml + helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods + if [[ $(helm status infisical) == *"FAILED"* ]]; then + echo "Helm upgrade failed" + exit 1 + else + echo "Helm upgrade was successful" + fi From 9742fdc77096b3bab1b3cfe5364da0aef5c30ade Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 13 Jun 2023 22:00:51 -0400 Subject: [PATCH 10/30] rename docker image --- .github/workflows/build-staging-img.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-staging-img.yml b/.github/workflows/build-staging-img.yml index 4f17aae65f..ca718a47dc 100644 --- a/.github/workflows/build-staging-img.yml +++ b/.github/workflows/build-staging-img.yml @@ -38,7 +38,7 @@ jobs: token: ${{ secrets.DEPOT_PROJECT_TOKEN }} load: true context: backend - tags: infisical/staging_deployment_backend:test + tags: infisical/backend:test - name: ⏻ Spawn backend container and dependencies run: | docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull From a4fb2378bb8c93f99eee1f06115b9b8e39fbd8eb Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 13 Jun 2023 22:06:53 -0400 Subject: [PATCH 11/30] wait for helm upgrade before mark complete --- .github/workflows/build-staging-img.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-staging-img.yml b/.github/workflows/build-staging-img.yml index ca718a47dc..73295fc54f 100644 --- a/.github/workflows/build-staging-img.yml +++ b/.github/workflows/build-staging-img.yml @@ -140,7 +140,7 @@ jobs: - name: Download helm values to file and upgrade gamma deploy run: | wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml - helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods + helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait if [[ $(helm status infisical) == *"FAILED"* ]]; then echo "Helm upgrade failed" exit 1 From 776cecc3efa346392abd28fd22ba61831460d664 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 13 Jun 2023 22:16:26 -0400 Subject: [PATCH 12/30] create prod release action --- ...age.yml => build-docker-image-to-prod.yml} | 39 +------------------ .github/workflows/build-staging-img.yml | 7 +--- 2 files changed, 2 insertions(+), 44 deletions(-) rename .github/workflows/{docker-image.yml => build-docker-image-to-prod.yml} (73%) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/build-docker-image-to-prod.yml similarity index 73% rename from .github/workflows/docker-image.yml rename to .github/workflows/build-docker-image-to-prod.yml index 87797c5f74..322a553c43 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/build-docker-image-to-prod.yml @@ -1,4 +1,4 @@ -name: Build, Publish and Deploy to Gamma +name: Release production images (frontend, backend) on: push: tags: @@ -116,40 +116,3 @@ jobs: platforms: linux/amd64,linux/arm64 build-args: | POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - gamma-deployment: - name: Deploy to gamma - runs-on: ubuntu-latest - needs: [frontend-image, backend-image] - steps: - - name: ☁️ Checkout source - uses: actions/checkout@v3 - - name: Install Helm - uses: azure/setup-helm@v3 - with: - version: v3.10.0 - - name: Install infisical helm chart - run: | - helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' - helm repo update - - name: Install kubectl - uses: azure/setup-kubectl@v3 - - name: Install doctl - uses: digitalocean/action-doctl@v2 - with: - token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - - name: Save DigitalOcean kubeconfig with short-lived credentials - run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179 - - name: switch to gamma namespace - run: kubectl config set-context --current --namespace=gamma - - name: test kubectl - run: kubectl get ingress - - name: Download helm values to file and upgrade gamma deploy - run: | - wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml - helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods - if [[ $(helm status infisical) == *"FAILED"* ]]; then - echo "Helm upgrade failed" - exit 1 - else - echo "Helm upgrade was successful" - fi diff --git a/.github/workflows/build-staging-img.yml b/.github/workflows/build-staging-img.yml index 73295fc54f..0e50d09068 100644 --- a/.github/workflows/build-staging-img.yml +++ b/.github/workflows/build-staging-img.yml @@ -1,10 +1,5 @@ name: Build, Publish and Deploy to Gamma -on: - workflow_dispatch: - push: - paths: - - "backend/**" - - "frontend/**" +on: [workflow_dispatch] jobs: backend-image: From 92647341a9453c5416ea8fccca14f8384c8543cd Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 14 Jun 2023 11:52:48 +0100 Subject: [PATCH 13/30] Update getPlan with workspace-specific consideration and add environmentLimit to returned plan --- .../controllers/v1/membershipOrgController.ts | 2 +- .../src/controllers/v1/workspaceController.ts | 2 +- .../controllers/v1/organizationsController.ts | 3 ++- backend/src/ee/routes/v1/organizations.ts | 3 ++- backend/src/ee/services/EELicenseService.ts | 24 ++++++++++++------- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index eafd7fae2a..0d4c1436d7 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -99,7 +99,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { throw new Error('Failed to validate organization membership'); } - const plan = await EELicenseService.getOrganizationPlan(organizationId); + const plan = await EELicenseService.getPlan(organizationId); if (plan.memberLimit !== null) { // case: limit imposed on number of members allowed diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index 796a7bebce..2faac0d686 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -116,7 +116,7 @@ export const createWorkspace = async (req: Request, res: Response) => { throw new Error("Failed to validate organization membership"); } - const plan = await EELicenseService.getOrganizationPlan(organizationId); + const plan = await EELicenseService.getPlan(organizationId); if (plan.workspaceLimit !== null) { // case: limit imposed on number of workspaces allowed diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 2dd212e771..2d2867d71c 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -8,8 +8,9 @@ import { EELicenseService } from '../../services'; */ export const getOrganizationPlan = async (req: Request, res: Response) => { const { organizationId } = req.params; + const workspaceId = req.query.workspaceId as string; - const plan = await EELicenseService.getOrganizationPlan(organizationId); + const plan = await EELicenseService.getPlan(organizationId, workspaceId); return res.status(200).send({ plan, diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index fd9fd08e94..711b5a17f2 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -5,7 +5,7 @@ import { requireOrganizationAuth, validateRequest } from '../../../middleware'; -import { param, body } from 'express-validator'; +import { param, body, query } from 'express-validator'; import { organizationsController } from '../../controllers/v1'; import { OWNER, ADMIN, MEMBER, ACCEPTED @@ -21,6 +21,7 @@ router.get( acceptedStatuses: [ACCEPTED] }), param('organizationId').exists().trim(), + query('workspaceId').optional().isString(), validateRequest, organizationsController.getOrganizationPlan ); diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index c98a5a449f..85a27de991 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -22,6 +22,8 @@ interface FeatureSet { workspacesUsed: number; memberLimit: number | null; membersUsed: number; + environmentLimit: number | null; + environmentsUsed: number; secretVersioning: boolean; pitRecovery: boolean; rbac: boolean; @@ -51,6 +53,8 @@ class EELicenseService { workspacesUsed: 0, memberLimit: null, membersUsed: 0, + environmentLimit: null, + environmentsUsed: 0, secretVersioning: true, pitRecovery: true, rbac: true, @@ -69,10 +73,10 @@ class EELicenseService { }); } - public async getOrganizationPlan(organizationId: string): Promise { + public async getPlan(organizationId: string, workspaceId?: string): Promise { try { if (this.instanceType === 'cloud') { - const cachedPlan = this.localFeatureSet.get(organizationId); + const cachedPlan = this.localFeatureSet.get(`${organizationId}-${workspaceId ?? ''}`); if (cachedPlan) { return cachedPlan; } @@ -80,12 +84,16 @@ class EELicenseService { const organization = await Organization.findById(organizationId); if (!organization) throw OrganizationNotFoundError(); - const { data: { currentPlan } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan` - ); + let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`; + + if (workspaceId) { + url += `?workspaceId=${workspaceId}`; + } + + const { data: { currentPlan } } = await licenseServerKeyRequest.get(url); // cache fetched plan for organization - this.localFeatureSet.set(organizationId, currentPlan); + this.localFeatureSet.set(`${organizationId}-${workspaceId ?? ''}`, currentPlan); return currentPlan; } @@ -96,10 +104,10 @@ class EELicenseService { return this.globalFeatureSet; } - public async refreshOrganizationPlan(organizationId: string) { + public async refreshOrganizationPlan(organizationId: string, workspaceId?: string) { if (this.instanceType === 'cloud') { this.localFeatureSet.del(organizationId); - await this.getOrganizationPlan(organizationId); + await this.getPlan(organizationId, workspaceId); } } From 82a026a426ebd027cd21ad0917c1557ccd25c7fa Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 14 Jun 2023 12:28:01 +0100 Subject: [PATCH 14/30] Update refreshPlan to consider workspace --- backend/src/controllers/v2/environmentController.ts | 7 ++++++- backend/src/ee/services/EELicenseService.ts | 4 ++-- backend/src/helpers/organization.ts | 2 +- backend/src/helpers/workspace.ts | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index d4f91bacec..d3ff5d0abc 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -8,6 +8,7 @@ import { Membership, } from '../../models'; import { SecretVersion } from '../../ee/models'; +import { EELicenseService } from '../../ee/services'; import { BadRequestError } from '../../utils/errors'; import _ from 'lodash'; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; @@ -40,6 +41,8 @@ export const createWorkspaceEnvironment = async ( }); await workspace.save(); + await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId); + return res.status(200).send({ message: 'Successfully created new environment', workspace: workspaceId, @@ -186,7 +189,9 @@ export const deleteWorkspaceEnvironment = async ( await Membership.updateMany( { workspace: workspaceId }, { $pull: { deniedPermissions: { environmentSlug: environmentSlug } } } - ) + ); + + await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId); return res.status(200).send({ message: 'Successfully deleted environment', diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 85a27de991..e8fe9253eb 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -104,9 +104,9 @@ class EELicenseService { return this.globalFeatureSet; } - public async refreshOrganizationPlan(organizationId: string, workspaceId?: string) { + public async refreshPlan(organizationId: string, workspaceId?: string) { if (this.instanceType === 'cloud') { - this.localFeatureSet.del(organizationId); + this.localFeatureSet.del(`${organizationId}-${workspaceId ?? ''}`); await this.getPlan(organizationId, workspaceId); } } diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 3e7f169d04..b88fbdf126 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -170,7 +170,7 @@ export const updateSubscriptionOrgQuantity = async ({ ); } - await EELicenseService.refreshOrganizationPlan(organizationId); + await EELicenseService.refreshPlan(organizationId); return stripeSubscription; }; \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 8b15d1baf6..694540f862 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -41,7 +41,7 @@ export const createWorkspace = async ({ workspaceId: workspace._id }); - await EELicenseService.refreshOrganizationPlan(organizationId); + await EELicenseService.refreshPlan(organizationId); return workspace; }; From 04b7383bbeb218aa9c73cb84aa2e143e0376d979 Mon Sep 17 00:00:00 2001 From: Eklal Budhathoki Date: Thu, 15 Jun 2023 00:17:00 +0545 Subject: [PATCH 15/30] fix: minor typos in code --- frontend/src/pages/password-reset.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index 450cd82de9..bdc83fea61 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -169,7 +169,7 @@ export default function PasswordReset() {

- You can find it in your emrgency kit. You had to download the enrgency kit during signup. + You can find it in your emergency kit. You had to download the emergency kit during signup.

From ccf0877b81ce7d1aff5849388632ee1fcd5f31fa Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 14 Jun 2023 17:15:37 -0400 Subject: [PATCH 16/30] Revert "Revert "add refresh token to cli"" This reverts commit 6b0e0f70d299ed8bf4fa23e4d70f8426e0a40a5f. --- cli/packages/api/api.go | 37 ++++++++++++++++++++++++++++++++ cli/packages/api/model.go | 5 +++++ cli/packages/cmd/login.go | 11 +++++----- cli/packages/models/cli.go | 7 +++--- cli/packages/util/credentials.go | 15 +++++++++++++ cli/packages/util/helper.go | 17 +++------------ 6 files changed, 70 insertions(+), 22 deletions(-) diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 5f8bc7aaed..4ace4c3877 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "net/http" "github.com/Infisical/infisical-merge/packages/config" "github.com/go-resty/resty/v2" @@ -179,6 +180,19 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo SetBody(request). Post(fmt.Sprintf("%v/v2/auth/login2", config.INFISICAL_URL)) + cookies := response.Cookies() + // Find a cookie by name + cookieName := "jid" + var refreshToken *http.Cookie + for _, cookie := range cookies { + if cookie.Name == cookieName { + refreshToken = cookie + break + } + } + + loginTwoV2Response.RefreshToken = refreshToken.Value + if err != nil { return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err) } @@ -247,3 +261,26 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib return accessibleEnvironmentsResponse, nil } + +func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToken string) (GetNewAccessTokenWithRefreshTokenResponse, error) { + var newAccessToken GetNewAccessTokenWithRefreshTokenResponse + response, err := httpClient. + R(). + SetResult(&newAccessToken). + SetHeader("User-Agent", USER_AGENT). + SetCookie(&http.Cookie{ + Name: "jid", + Value: refreshToken, + }). + Post(fmt.Sprintf("%v/v1/auth/token", config.INFISICAL_URL)) + + if err != nil { + return GetNewAccessTokenWithRefreshTokenResponse{}, err + } + + if response.IsError() { + return GetNewAccessTokenWithRefreshTokenResponse{}, fmt.Errorf("CallGetNewAccessTokenWithRefreshToken: Unsuccessful response: [response=%v]", response) + } + + return newAccessToken, nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 71354c84c7..bce2a34d61 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -281,6 +281,7 @@ type GetLoginTwoV2Response struct { ProtectedKey string `json:"protectedKey"` ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyTag string `json:"protectedKeyTag"` + RefreshToken string `json:"RefreshToken"` } type VerifyMfaTokenRequest struct { @@ -314,3 +315,7 @@ type VerifyMfaTokenErrorResponse struct { Application string `json:"application"` Extra []interface{} `json:"extra"` } + +type GetNewAccessTokenWithRefreshTokenResponse struct { + Token string `json:"token"` +} diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index f20a178539..ab0f531c23 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -97,7 +97,7 @@ var loginCmd = &cobra.Command{ loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) if err != nil { - fmt.Println("Unable to authenticate with the provided credentials, please try again") + log.Warn().Msg("Unable to authenticate with the provided credentials, please ensure your email and password are correct") log.Debug().Err(err) return } @@ -244,9 +244,10 @@ var loginCmd = &cobra.Command{ } userCredentialsToBeStored := &models.UserCredentials{ - Email: email, - PrivateKey: string(decryptedPrivateKey), - JTWToken: loginTwoResponse.Token, + Email: email, + PrivateKey: string(decryptedPrivateKey), + JTWToken: loginTwoResponse.Token, + RefreshToken: loginTwoResponse.RefreshToken, } err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored) @@ -414,7 +415,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R }) if err != nil { - util.HandleError(err) + return nil, nil, err } // **** Login 2 diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index b9b0ab7b59..22f72e2da2 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -5,9 +5,10 @@ import ( ) type UserCredentials struct { - Email string `json:"email"` - PrivateKey string `json:"privateKey"` - JTWToken string `json:"JTWToken"` + Email string `json:"email"` + PrivateKey string `json:"privateKey"` + JTWToken string `json:"JTWToken"` + RefreshToken string `json:"RefreshToken"` } // The file struct for Infisical config file diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 9147a717ac..6b203c2e3f 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -9,6 +9,7 @@ import ( "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" ) type LoggedInUserDetails struct { @@ -96,6 +97,20 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { } isAuthenticated := api.CallIsAuthenticated(httpClient) + + if !isAuthenticated { + accessTokenResponse, _ := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) + if accessTokenResponse.Token != "" { + isAuthenticated = true + userCreds.JTWToken = accessTokenResponse.Token + } + } + + err = StoreUserCredsInKeyRing(&userCreds) + if err != nil { + log.Debug().Msg("unable to store your user credentials with new access token") + } + if !isAuthenticated { return LoggedInUserDetails{ IsUserLoggedIn: true, // was logged in diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 8527c4059d..d96ac38995 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -74,23 +74,12 @@ func ConfigContainsEmail(users []models.LoggedInUser, email string) bool { } func RequireLogin() { - currentUserDetails, err := GetCurrentLoggedInUserDetails() + // get the config file that stores the current logged in user email + configFile, _ := GetConfigFile() - if err != nil { - HandleError(err, "unable to retrieve your login details") - } - - if !currentUserDetails.IsUserLoggedIn { + if configFile.LoggedInUserEmail == "" { PrintErrorMessageAndExit("You must be logged in to run this command. To login, run [infisical login]") } - - if currentUserDetails.LoginExpired { - PrintErrorMessageAndExit("Your login expired, please login in again. To login, run [infisical login]") - } - - if currentUserDetails.UserCredentials.Email == "" && currentUserDetails.UserCredentials.JTWToken == "" && currentUserDetails.UserCredentials.PrivateKey == "" { - PrintErrorMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]") - } } func RequireServiceToken() { From 7088b3c9d89ea2c24ca632a9fda16bd04b161a04 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 14 Jun 2023 17:31:47 -0400 Subject: [PATCH 17/30] patch refresh token cli --- cli/packages/api/api.go | 21 ++++++++++++++++++++- cli/packages/api/model.go | 1 + cli/packages/cmd/login.go | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 4ace4c3877..87832a5378 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -160,6 +160,22 @@ func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest) SetBody(request). Post(fmt.Sprintf("%v/v2/auth/mfa/verify", config.INFISICAL_URL)) + cookies := response.Cookies() + // Find a cookie by name + cookieName := "jid" + var refreshToken *http.Cookie + for _, cookie := range cookies { + if cookie.Name == cookieName { + refreshToken = cookie + break + } + } + + // When MFA is enabled + if refreshToken != nil { + verifyMfaTokenResponse.RefreshToken = refreshToken.Value + } + if err != nil { return nil, nil, fmt.Errorf("CallVerifyMfaToken: Unable to complete api request [err=%s]", err) } @@ -191,7 +207,10 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo } } - loginTwoV2Response.RefreshToken = refreshToken.Value + // When MFA is enabled + if refreshToken != nil { + loginTwoV2Response.RefreshToken = refreshToken.Value + } if err != nil { return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err) diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index bce2a34d61..30896b819d 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -299,6 +299,7 @@ type VerifyMfaTokenResponse struct { ProtectedKey string `json:"protectedKey"` ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyTag string `json:"protectedKeyTag"` + RefreshToken string `json:"refreshToken"` } type VerifyMfaTokenErrorResponse struct { diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index ab0f531c23..1329b1d2a0 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -143,7 +143,7 @@ var loginCmd = &cobra.Command{ loginTwoResponse.Tag = verifyMFAresponse.Tag loginTwoResponse.Token = verifyMFAresponse.Token loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion - + loginTwoResponse.RefreshToken = verifyMFAresponse.RefreshToken break } } From 391e37d49e3cbe388c5476ac221c0cbce457a5e8 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 14 Jun 2023 21:27:37 -0700 Subject: [PATCH 18/30] fixed bugs with env and password reset --- backend/src/ee/services/EELicenseService.ts | 4 +--- frontend/src/components/login/InitialLoginStep.tsx | 10 ++++++++-- frontend/src/hooks/api/subscriptions/types.ts | 2 +- .../ProjectSettingsPage/ProjectSettingsPage.tsx | 7 +++---- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index e8fe9253eb..89e71e57bd 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -30,7 +30,6 @@ interface FeatureSet { customRateLimits: boolean; customAlerts: boolean; auditLogs: boolean; - envLimit?: number | null; } /** @@ -60,8 +59,7 @@ class EELicenseService { rbac: true, customRateLimits: true, customAlerts: true, - auditLogs: false, - envLimit: null + auditLogs: false } public localFeatureSet: NodeCache; diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index 77e83434e5..3dcdf968be 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -76,7 +76,7 @@ export default function InitialLoginStep({ {t('login.continue-with-google')}
*/} -
+
-
+
{t('login.create-account')}
+
+ Forgot password? + + Recover your account + +
} diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 61736b4f4e..af1b315d1d 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -12,5 +12,5 @@ export type SubscriptionPlan = { tier: number; workspaceLimit: number; workspacesUsed: number; - envLimit: number; + environmentLimit: number; }; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx index ee9b85cc58..808a74bd82 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -41,12 +41,11 @@ import { CreateServiceToken, CreateUpdateEnvFormData, CreateWsTag, + E2EESection, EnvironmentSection, ProjectIndexSecretsSection, ProjectNameChangeSection, - ServiceTokenSection, - E2EESection -} from './components'; + ServiceTokenSection} from './components'; export const ProjectSettingsPage = () => { const { t } = useTranslation(); @@ -91,7 +90,7 @@ export const ProjectSettingsPage = () => { // get user subscription const { subscription } = useSubscription(); const host = window.location.origin; - const isEnvServiceAllowed = ((currentWorkspace?.environments || []).length < (subscription?.envLimit || 3) || host !== 'https://app.infisical.com'); + const isEnvServiceAllowed = (((currentWorkspace?.environments || []).length < (subscription?.environmentLimit || 3)) || host !== 'https://app.infisical.com'); const onRenameWorkspace = async (name: string) => { try { From ccaf9a9ffcacb611704c56982b163d23bee14f63 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 15 Jun 2023 15:48:19 +0100 Subject: [PATCH 19/30] Update implementation for environment limit paywall --- .../controllers/v2/environmentController.ts | 19 ++++++++++++++++++- .../ProjectSettingsPage.tsx | 4 ++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index d3ff5d0abc..bf6d7d3c59 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -9,7 +9,7 @@ import { } from '../../models'; import { SecretVersion } from '../../ee/models'; import { EELicenseService } from '../../ee/services'; -import { BadRequestError } from '../../utils/errors'; +import { BadRequestError, WorkspaceNotFoundError } from '../../utils/errors'; import _ from 'lodash'; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; @@ -23,9 +23,26 @@ export const createWorkspaceEnvironment = async ( req: Request, res: Response ) => { + const { workspaceId } = req.params; const { environmentName, environmentSlug } = req.body; const workspace = await Workspace.findById(workspaceId).exec(); + + if (!workspace) throw WorkspaceNotFoundError(); + + const plan = await EELicenseService.getPlan(workspace.organization.toString()); + + if (plan.environmentLimit !== null) { + // case: limit imposed on number of environments allowed + if (workspace.environments.length >= plan.environmentLimit) { + // case: number of environments used exceeds the number of environments allowed + + return res.status(400).send({ + message: 'Failed to create environment due to environment limit reached. Upgrade plan to create more environments.' + }); + } + } + if ( !workspace || workspace?.environments.find( diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx index 808a74bd82..9e9ead8b14 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -89,8 +89,8 @@ export const ProjectSettingsPage = () => { // get user subscription const { subscription } = useSubscription(); - const host = window.location.origin; - const isEnvServiceAllowed = (((currentWorkspace?.environments || []).length < (subscription?.environmentLimit || 3)) || host !== 'https://app.infisical.com'); + + const isEnvServiceAllowed = (subscription?.environmentLimit && currentWorkspace?.environments) ? (currentWorkspace.environments.length < subscription.environmentLimit) : true; const onRenameWorkspace = async (name: string) => { try { From 24714185910d07bc302678eb2ae5475cd47aac7e Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Thu, 15 Jun 2023 22:38:26 +0530 Subject: [PATCH 20/30] feat(folder-scoped-integration): implemented api changes for integrations to support folders --- .../controllers/v1/integrationController.ts | 62 ++- backend/src/helpers/bot.ts | 98 ++-- backend/src/helpers/integration.ts | 523 ++++++++++-------- backend/src/models/integration.ts | 54 +- backend/src/routes/v1/integration.ts | 124 +++-- backend/src/services/BotService.ts | 204 +++---- backend/src/utils/setup/backfillData.ts | 17 + backend/src/utils/setup/index.ts | 2 + 8 files changed, 599 insertions(+), 485 deletions(-) diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 5119761d0b..63828ef148 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,10 +1,11 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import { - Integration -} from '../../models'; -import { EventService } from '../../services'; -import { eventPushSecrets } from '../../events'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Integration } from "../../models"; +import { EventService } from "../../services"; +import { eventPushSecrets } from "../../events"; +import Folder from "../../models/folder"; +import { getFolderByPath } from "../../services/FolderService"; +import { BadRequestError } from "../../utils/errors"; /** * Create/initialize an (empty) integration for integration authorization @@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => { targetServiceId, owner, path, - region + region, + secretPath, } = req.body; - + + const folders = await Folder.findOne({ + workspace: req.integrationAuth.workspace._id, + environment: sourceEnvironment, + }); + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw BadRequestError({ + message: "Path for service token does not exist", + }); + } + } + // TODO: validate [sourceEnvironment] and [targetEnvironment] // initialize new integration after saving integration access token @@ -44,17 +60,18 @@ export const createIntegration = async (req: Request, res: Response) => { owner, path, region, + secretPath, integration: req.integrationAuth.integration, - integrationAuth: new Types.ObjectId(integrationAuthId) + integrationAuth: new Types.ObjectId(integrationAuthId), }).save(); - + if (integration) { // trigger event - push secrets EventService.handleEvent({ event: eventPushSecrets({ workspaceId: integration.workspace, - environment: sourceEnvironment - }) + environment: sourceEnvironment, + }), }); } @@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => { * @returns */ export const updateIntegration = async (req: Request, res: Response) => { - // TODO: add integration-specific validation to ensure that each // integration has the correct fields populated in [Integration] @@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => { appId, targetEnvironment, owner, // github-specific integration param + secretPath, } = req.body; + const folders = await Folder.findOne({ + workspace: req.integration.workspace, + environment, + }); + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw BadRequestError({ + message: "Path for service token does not exist", + }); + } + } + const integration = await Integration.findOneAndUpdate( { _id: req.integration._id, @@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => { appId, targetEnvironment, owner, + secretPath, }, { new: true, @@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => { EventService.handleEvent({ event: eventPushSecrets({ workspaceId: integration.workspace, - environment + environment, }), }); } diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 439f7c531d..8c3b5b8163 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -1,29 +1,21 @@ import { Types } from "mongoose"; -import { - Bot, - BotKey, - Secret, - ISecret, - IUser -} from "../models"; +import { Bot, BotKey, Secret, ISecret, IUser } from "../models"; import { generateKeyPair, encryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8, - decryptAsymmetric -} from '../utils/crypto'; + decryptAsymmetric, +} from "../utils/crypto"; import { SECRET_SHARED, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + ENCODING_SCHEME_BASE64, } from "../variables"; -import { - getEncryptionKey, - getRootEncryptionKey, - client -} from "../config"; +import { getEncryptionKey, getRootEncryptionKey, client } from "../config"; import { InternalServerError } from "../utils/errors"; +import Folder from "../models/folder"; +import { getFolderByPath } from "../services/FolderService"; /** * Create an inactive bot with name [name] for workspace with id [workspaceId] @@ -40,15 +32,14 @@ export const createBot = async ({ }) => { const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); - + const { publicKey, privateKey } = generateKeyPair(); - + if (rootEncryptionKey) { - const { - ciphertext, - iv, - tag - } = client.encryptSymmetric(privateKey, rootEncryptionKey); + const { ciphertext, iv, tag } = client.encryptSymmetric( + privateKey, + rootEncryptionKey + ); return await new Bot({ name, @@ -59,9 +50,8 @@ export const createBot = async ({ iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 + keyEncoding: ENCODING_SCHEME_BASE64, }).save(); - } else if (encryptionKey) { const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: privateKey, @@ -77,12 +67,12 @@ export const createBot = async ({ iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 + keyEncoding: ENCODING_SCHEME_UTF8, }).save(); } throw InternalServerError({ - message: 'Failed to create new bot due to missing encryption key' + message: "Failed to create new bot due to missing encryption key", }); }; @@ -92,11 +82,11 @@ export const createBot = async ({ */ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { const botKey = await BotKey.exists({ - workspace: workspaceId - }); - + workspace: workspaceId, + }); + return botKey ? false : true; -} +}; /** * Return decrypted secrets for workspace with id [workspaceId] @@ -108,16 +98,38 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { export const getSecretsBotHelper = async ({ workspaceId, environment, + secretPath, }: { workspaceId: Types.ObjectId; environment: string; + secretPath: string; }) => { const content = {} as any; const key = await getKey({ workspaceId: workspaceId }); + + let folderId = "root"; + const folders = await Folder.findOne({ + workspace: workspaceId, + environment, + }); + + if (!folders && secretPath !== "/") { + throw InternalServerError({ message: "Folder not found" }); + } + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw InternalServerError({ message: "Folder not found" }); + } + folderId = folder.id; + } + const secrets = await Secret.find({ workspace: workspaceId, environment, type: SECRET_SHARED, + folder: folderId, }); secrets.forEach((secret: ISecret) => { @@ -148,14 +160,17 @@ export const getSecretsBotHelper = async ({ * @param {String} obj.workspaceId - id of workspace * @returns {String} key - decrypted workspace key */ -export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => { +export const getKey = async ({ + workspaceId, +}: { + workspaceId: Types.ObjectId; +}) => { const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); const botKey = await BotKey.findOne({ workspace: workspaceId, - }) - .populate<{ sender: IUser }>("sender", "publicKey"); + }).populate<{ sender: IUser }>("sender", "publicKey"); if (!botKey) throw new Error("Failed to find bot key"); @@ -168,7 +183,12 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) = if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) { // case: encoding scheme is base64 - const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag); + const privateKeyBot = client.decryptSymmetric( + bot.encryptedPrivateKey, + rootEncryptionKey, + bot.iv, + bot.tag + ); return decryptAsymmetric({ ciphertext: botKey.encryptedKey, @@ -177,15 +197,14 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) = privateKey: privateKeyBot, }); } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) { - // case: encoding scheme is utf8 const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ ciphertext: bot.encryptedPrivateKey, iv: bot.iv, tag: bot.tag, - key: encryptionKey + key: encryptionKey, }); - + return decryptAsymmetric({ ciphertext: botKey.encryptedKey, nonce: botKey.nonce, @@ -195,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) = } throw InternalServerError({ - message: "Failed to obtain bot's copy of workspace key needed for bot operations" + message: + "Failed to obtain bot's copy of workspace key needed for bot operations", }); }; @@ -254,4 +274,4 @@ export const decryptSymmetricHelper = async ({ }); return plaintext; -}; \ No newline at end of file +}; diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index b6f4b0915d..baf41d762c 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -1,26 +1,20 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; +import { Bot, Integration, IntegrationAuth } from "../models"; +import { exchangeCode, exchangeRefresh, syncSecrets } from "../integrations"; +import { BotService } from "../services"; import { - Bot, - Integration, - IntegrationAuth -} from '../models'; -import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; -import { BotService } from '../services'; -import { - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8 -} from '../variables'; -import { - UnauthorizedRequestError, -} from '../utils/errors'; + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, +} from "../variables"; +import { UnauthorizedRequestError } from "../utils/errors"; interface Update { - workspace: string; - integration: string; - teamId?: string; - accountId?: string; + workspace: string; + integration: string; + teamId?: string; + accountId?: string; } /** @@ -31,78 +25,83 @@ interface Update { * - Create bot sequence for integration * @param {Object} obj * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.integration - name of integration + * @param {String} obj.integration - name of integration * @param {String} obj.code - code * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange -*/ + */ export const handleOAuthExchangeHelper = async ({ - workspaceId, + workspaceId, + integration, + code, + environment, +}: { + workspaceId: string; + integration: string; + code: string; + environment: string; +}) => { + const bot = await Bot.findOne({ + workspace: workspaceId, + isActive: true, + }); + + if (!bot) + throw new Error("Bot must be enabled for OAuth2 code-token exchange"); + + // exchange code for access and refresh tokens + const res = await exchangeCode({ integration, code, - environment -}: { - workspaceId: string; - integration: string; - code: string; - environment: string; -}) => { - const bot = await Bot.findOne({ - workspace: workspaceId, - isActive: true + }); + + const update: Update = { + workspace: workspaceId, + integration, + }; + + switch (integration) { + case INTEGRATION_VERCEL: + update.teamId = res.teamId; + break; + case INTEGRATION_NETLIFY: + update.accountId = res.accountId; + break; + } + + const integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + workspace: workspaceId, + integration, + }, + update, + { + new: true, + upsert: true, + } + ); + + if (res.refreshToken) { + // case: refresh token returned from exchange + // set integration auth refresh token + await setIntegrationAuthRefreshHelper({ + integrationAuthId: integrationAuth._id.toString(), + refreshToken: res.refreshToken, }); - - if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange'); - - // exchange code for access and refresh tokens - const res = await exchangeCode({ - integration, - code + } + + if (res.accessToken) { + // case: access token returned from exchange + // set integration auth access token + await setIntegrationAuthAccessHelper({ + integrationAuthId: integrationAuth._id.toString(), + accessId: null, + accessToken: res.accessToken, + accessExpiresAt: res.accessExpiresAt, }); - - const update: Update = { - workspace: workspaceId, - integration - } - - switch (integration) { - case INTEGRATION_VERCEL: - update.teamId = res.teamId; - break; - case INTEGRATION_NETLIFY: - update.accountId = res.accountId; - break; - } - - const integrationAuth = await IntegrationAuth.findOneAndUpdate({ - workspace: workspaceId, - integration - }, update, { - new: true, - upsert: true - }); - - if (res.refreshToken) { - // case: refresh token returned from exchange - // set integration auth refresh token - await setIntegrationAuthRefreshHelper({ - integrationAuthId: integrationAuth._id.toString(), - refreshToken: res.refreshToken - }); - } - - if (res.accessToken) { - // case: access token returned from exchange - // set integration auth access token - await setIntegrationAuthAccessHelper({ - integrationAuthId: integrationAuth._id.toString(), - accessId: null, - accessToken: res.accessToken, - accessExpiresAt: res.accessExpiresAt - }); - } - - return integrationAuth; -} + } + + return integrationAuth; +}; /** * Sync/push environment variables in workspace with id [workspaceId] to * all active integrations for that workspace @@ -110,48 +109,54 @@ export const handleOAuthExchangeHelper = async ({ * @param {Object} obj.workspaceId - id of workspace */ export const syncIntegrationsHelper = async ({ - workspaceId, - environment + workspaceId, + environment, }: { - workspaceId: Types.ObjectId; - environment?: string; + workspaceId: Types.ObjectId; + environment?: string; }) => { - const integrations = await Integration.find({ - workspace: workspaceId, - ...(environment ? { - environment - } : {}), - isActive: true, - app: { $ne: null } + const integrations = await Integration.find({ + workspace: workspaceId, + ...(environment + ? { + environment, + } + : {}), + isActive: true, + app: { $ne: null }, + }); + + // for each workspace integration, sync/push secrets + // to that integration + for await (const integration of integrations) { + // get workspace, environment (shared) secrets + const secrets = await BotService.getSecrets({ + // issue here? + workspaceId: integration.workspace, + environment: integration.environment, + secretPath: integration.secretPath, }); - // for each workspace integration, sync/push secrets - // to that integration - for await (const integration of integrations) { - // get workspace, environment (shared) secrets - const secrets = await BotService.getSecrets({ // issue here? - workspaceId: integration.workspace, - environment: integration.environment - }); + const integrationAuth = await IntegrationAuth.findById( + integration.integrationAuth + ); + if (!integrationAuth) throw new Error("Failed to find integration auth"); - const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - // get integration auth access token - const access = await getIntegrationAuthAccessHelper({ - integrationAuthId: integration.integrationAuth - }); + // get integration auth access token + const access = await getIntegrationAuthAccessHelper({ + integrationAuthId: integration.integrationAuth, + }); - // sync secrets to integration - await syncSecrets({ - integration, - integrationAuth, - secrets, - accessId: access.accessId === undefined ? null : access.accessId, - accessToken: access.accessToken - }); - } -} + // sync secrets to integration + await syncSecrets({ + integration, + integrationAuth, + secrets, + accessId: access.accessId === undefined ? null : access.accessId, + accessToken: access.accessToken, + }); + } +}; /** * Return decrypted refresh token using the bot's copy @@ -161,22 +166,29 @@ export const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @param {String} refreshToken - decrypted refresh token */ -export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .select('+refreshCiphertext +refreshIV +refreshTag'); +export const getIntegrationAuthRefreshHelper = async ({ + integrationAuthId, +}: { + integrationAuthId: Types.ObjectId; +}) => { + const integrationAuth = await IntegrationAuth.findById( + integrationAuthId + ).select("+refreshCiphertext +refreshIV +refreshTag"); - if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); - - const refreshToken = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.refreshCiphertext as string, - iv: integrationAuth.refreshIV as string, - tag: integrationAuth.refreshTag as string + if (!integrationAuth) + throw UnauthorizedRequestError({ + message: "Failed to locate Integration Authentication credentials", }); - - return refreshToken; -} + + const refreshToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.refreshCiphertext as string, + iv: integrationAuth.refreshIV as string, + tag: integrationAuth.refreshTag as string, + }); + + return refreshToken; +}; /** * Return decrypted access token using the bot's copy @@ -186,50 +198,65 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i * @param {String} obj.integrationAuthId - id of integration auth * @returns {String} accessToken - decrypted access token */ -export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { - let accessId; - let accessToken; - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag'); +export const getIntegrationAuthAccessHelper = async ({ + integrationAuthId, +}: { + integrationAuthId: Types.ObjectId; +}) => { + let accessId; + let accessToken; + const integrationAuth = await IntegrationAuth.findById( + integrationAuthId + ).select( + "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag" + ); - if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); - - accessToken = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.accessCiphertext as string, - iv: integrationAuth.accessIV as string, - tag: integrationAuth.accessTag as string + if (!integrationAuth) + throw UnauthorizedRequestError({ + message: "Failed to locate Integration Authentication credentials", }); - if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { - // there is a access token expiration date - // and refresh token to exchange with the OAuth2 server - - if (integrationAuth.accessExpiresAt < new Date()) { - // access token is expired - const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); - accessToken = await exchangeRefresh({ - integrationAuth, - refreshToken - }); - } + accessToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.accessCiphertext as string, + iv: integrationAuth.accessIV as string, + tag: integrationAuth.accessTag as string, + }); + + if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { + // there is a access token expiration date + // and refresh token to exchange with the OAuth2 server + + if (integrationAuth.accessExpiresAt < new Date()) { + // access token is expired + const refreshToken = await getIntegrationAuthRefreshHelper({ + integrationAuthId, + }); + accessToken = await exchangeRefresh({ + integrationAuth, + refreshToken, + }); } - - if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) { - accessId = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.accessIdCiphertext as string, - iv: integrationAuth.accessIdIV as string, - tag: integrationAuth.accessIdTag as string - }); - } - - return ({ - accessId, - accessToken + } + + if ( + integrationAuth?.accessIdCiphertext && + integrationAuth?.accessIdIV && + integrationAuth?.accessIdTag + ) { + accessId = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.accessIdCiphertext as string, + iv: integrationAuth.accessIdIV as string, + tag: integrationAuth.accessIdTag as string, }); -} + } + + return { + accessId, + accessToken, + }; +}; /** * Encrypt refresh token [refreshToken] using the bot's copy @@ -240,41 +267,43 @@ export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { in * @param {String} obj.refreshToken - refresh token */ export const setIntegrationAuthRefreshHelper = async ({ - integrationAuthId, - refreshToken + integrationAuthId, + refreshToken, }: { - integrationAuthId: string; - refreshToken: string; + integrationAuthId: string; + refreshToken: string; }) => { - - let integrationAuth = await IntegrationAuth - .findById(integrationAuthId); - - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - const obj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: refreshToken - }); - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - _id: integrationAuthId - }, { - refreshCiphertext: obj.ciphertext, - refreshIV: obj.iv, - refreshTag: obj.tag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true - }); - - return integrationAuth; -} + let integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) throw new Error("Failed to find integration auth"); + + const obj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: refreshToken, + }); + + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + _id: integrationAuthId, + }, + { + refreshCiphertext: obj.ciphertext, + refreshIV: obj.iv, + refreshTag: obj.tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + { + new: true, + } + ); + + return integrationAuth; +}; /** * Encrypt access token [accessToken] and (optionally) access id [accessId] - * using the bot's copy of the workspace key for workspace belonging to + * using the bot's copy of the workspace key for workspace belonging to * integration auth with id [integrationAuthId] and store it along with [accessExpiresAt] * @param {Object} obj * @param {String} obj.integrationAuthId - id of integration auth @@ -282,48 +311,52 @@ export const setIntegrationAuthRefreshHelper = async ({ * @param {Date} obj.accessExpiresAt - expiration date of access token */ export const setIntegrationAuthAccessHelper = async ({ - integrationAuthId, - accessId, - accessToken, - accessExpiresAt + integrationAuthId, + accessId, + accessToken, + accessExpiresAt, }: { - integrationAuthId: string; - accessId: string | null; - accessToken: string; - accessExpiresAt: Date | undefined; + integrationAuthId: string; + accessId: string | null; + accessToken: string; + accessExpiresAt: Date | undefined; }) => { - let integrationAuth = await IntegrationAuth.findById(integrationAuthId); - - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - const encryptedAccessTokenObj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: accessToken + let integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) throw new Error("Failed to find integration auth"); + + const encryptedAccessTokenObj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: accessToken, + }); + + let encryptedAccessIdObj; + if (accessId) { + encryptedAccessIdObj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: accessId, }); - - let encryptedAccessIdObj; - if (accessId) { - encryptedAccessIdObj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: accessId - }); + } + + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + _id: integrationAuthId, + }, + { + accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, + accessIdIV: encryptedAccessIdObj?.iv ?? undefined, + accessIdTag: encryptedAccessIdObj?.tag ?? undefined, + accessCiphertext: encryptedAccessTokenObj.ciphertext, + accessIV: encryptedAccessTokenObj.iv, + accessTag: encryptedAccessTokenObj.tag, + accessExpiresAt, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + { + new: true, } - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - _id: integrationAuthId - }, { - accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, - accessIdIV: encryptedAccessIdObj?.iv ?? undefined, - accessIdTag: encryptedAccessIdObj?.tag ?? undefined, - accessCiphertext: encryptedAccessTokenObj.ciphertext, - accessIV: encryptedAccessTokenObj.iv, - accessTag: encryptedAccessTokenObj.tag, - accessExpiresAt, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true - }); - - return integrationAuth; -} \ No newline at end of file + ); + + return integrationAuth; +}; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d4fbae807f..c23cf69f18 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -15,7 +15,7 @@ import { INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_HASHICORP_VAULT, } from "../variables"; export interface IIntegration { @@ -33,23 +33,24 @@ export interface IIntegration { targetServiceId: string; path: string; region: string; + secretPath: string; integration: - | 'azure-key-vault' - | 'aws-parameter-store' - | 'aws-secret-manager' - | 'heroku' - | 'vercel' - | 'netlify' - | 'github' - | 'gitlab' - | 'render' - | 'railway' - | 'flyio' - | 'circleci' - | 'travisci' - | 'supabase' - | 'checkly' - | 'hashicorp-vault'; + | "azure-key-vault" + | "aws-parameter-store" + | "aws-secret-manager" + | "heroku" + | "vercel" + | "netlify" + | "github" + | "gitlab" + | "render" + | "railway" + | "flyio" + | "circleci" + | "travisci" + | "supabase" + | "checkly" + | "hashicorp-vault"; integrationAuth: Types.ObjectId; } @@ -71,7 +72,7 @@ const integrationSchema = new Schema( url: { // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) type: String, - default: null + default: null, }, app: { // name of app in provider @@ -90,17 +91,17 @@ const integrationSchema = new Schema( }, targetEnvironmentId: { type: String, - default: null + default: null, }, targetService: { // railway-specific service type: String, - default: null + default: null, }, targetServiceId: { // railway-specific service type: String, - default: null + default: null, }, owner: { // github-specific repo owner-login @@ -111,12 +112,12 @@ const integrationSchema = new Schema( // aws-parameter-store-specific path // (also) vercel preview-branch type: String, - default: null + default: null, }, region: { // aws-parameter-store-specific path type: String, - default: null + default: null, }, integration: { type: String, @@ -136,7 +137,7 @@ const integrationSchema = new Schema( INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_HASHICORP_VAULT, ], required: true, }, @@ -145,6 +146,11 @@ const integrationSchema = new Schema( ref: "IntegrationAuth", required: true, }, + secretPath: { + type: String, + required: true, + default: "/", + }, }, { timestamps: true, diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index b8e0b38bdc..c4ba329c0c 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -1,75 +1,77 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { - requireAuth, - requireIntegrationAuth, - requireIntegrationAuthorizationAuth, - validateRequest -} from '../../middleware'; + requireAuth, + requireIntegrationAuth, + requireIntegrationAuthorizationAuth, + validateRequest, +} from "../../middleware"; import { - ADMIN, - MEMBER, - AUTH_MODE_JWT, - AUTH_MODE_API_KEY -} from '../../variables'; -import { body, param } from 'express-validator'; -import { integrationController } from '../../controllers/v1'; + ADMIN, + MEMBER, + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, +} from "../../variables"; +import { body, param } from "express-validator"; +import { integrationController } from "../../controllers/v1"; router.post( - '/', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - location: 'body' - }), - body('integrationAuthId').exists().isString().trim(), - body('app').trim(), - body('isActive').exists().isBoolean(), - body('appId').trim(), - body('sourceEnvironment').trim(), - body('targetEnvironment').trim(), - body('targetEnvironmentId').trim(), - body('targetService').trim(), - body('targetServiceId').trim(), - body('owner').trim(), - body('path').trim(), - body('region').trim(), - validateRequest, - integrationController.createIntegration + "/", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER], + location: "body", + }), + body("integrationAuthId").exists().isString().trim(), + body("app").trim(), + body("isActive").exists().isBoolean(), + body("appId").trim(), + body("secretPath").default("/").isString().trim(), + body("sourceEnvironment").trim(), + body("targetEnvironment").trim(), + body("targetEnvironmentId").trim(), + body("targetService").trim(), + body("targetServiceId").trim(), + body("owner").trim(), + body("path").trim(), + body("region").trim(), + validateRequest, + integrationController.createIntegration ); router.patch( - '/:integrationId', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] - }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param('integrationId').exists().trim(), - body('isActive').exists().isBoolean(), - body('app').exists().trim(), - body('environment').exists().trim(), - body('appId').exists(), - body('targetEnvironment').exists(), - body('owner').exists(), - validateRequest, - integrationController.updateIntegration + "/:integrationId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT], + }), + requireIntegrationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationId").exists().trim(), + body("isActive").exists().isBoolean(), + body("app").exists().trim(), + body("secretPath").default("/").isString().trim(), + body("environment").exists().trim(), + body("appId").exists(), + body("targetEnvironment").exists(), + body("owner").exists(), + validateRequest, + integrationController.updateIntegration ); router.delete( - '/:integrationId', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] - }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param('integrationId').exists().trim(), - validateRequest, - integrationController.deleteIntegration + "/:integrationId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT], + }), + requireIntegrationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationId").exists().trim(), + validateRequest, + integrationController.deleteIntegration ); export default router; diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts index 7081a64cbf..0e3768a9c9 100644 --- a/backend/src/services/BotService.ts +++ b/backend/src/services/BotService.ts @@ -1,110 +1,112 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - getSecretsBotHelper, - encryptSymmetricHelper, - decryptSymmetricHelper, - getKey, - getIsWorkspaceE2EEHelper -} from '../helpers/bot'; + getSecretsBotHelper, + encryptSymmetricHelper, + decryptSymmetricHelper, + getKey, + getIsWorkspaceE2EEHelper, +} from "../helpers/bot"; /** * Class to handle bot actions */ class BotService { - - /** - * Return whether or not workspace with id [workspaceId] is end-to-end encrypted - * @param workspaceId - id of workspace - * @returns {Boolean} - */ - static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) { - return await getIsWorkspaceE2EEHelper(workspaceId); - } + /** + * Return whether or not workspace with id [workspaceId] is end-to-end encrypted + * @param workspaceId - id of workspace + * @returns {Boolean} + */ + static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) { + return await getIsWorkspaceE2EEHelper(workspaceId); + } - /** - * Get workspace key for workspace with id [workspaceId] shared to bot. - * @param {Object} obj - * @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for - * @returns - */ - static async getWorkspaceKeyWithBot({ - workspaceId - }: { - workspaceId: Types.ObjectId; - }) { - return await getKey({ - workspaceId - }); - } - - /** - * Return decrypted secrets for workspace with id [workspaceId] and - * environment [environmen] shared to bot. - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace of secrets - * @param {String} obj.environment - environment for secrets - * @returns {Object} secretObj - object where keys are secret keys and values are secret values - */ - static async getSecrets({ - workspaceId, - environment - }: { - workspaceId: Types.ObjectId; - environment: string; - }) { - return await getSecretsBotHelper({ - workspaceId, - environment - }); - } - - /** - * Return symmetrically encrypted [plaintext] using the - * bot's copy of the workspace key for workspace with id [workspaceId] - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.plaintext - plaintext to encrypt - */ - static async encryptSymmetric({ - workspaceId, - plaintext - }: { - workspaceId: Types.ObjectId; - plaintext: string; - }) { - return await encryptSymmetricHelper({ - workspaceId, - plaintext - }); - } - - /** - * Return symmetrically decrypted [ciphertext] using the - * bot's copy of the workspace key for workspace with id [workspaceId] - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.ciphertext - ciphertext to decrypt - * @param {String} obj.iv - iv - * @param {String} obj.tag - tag - */ - static async decryptSymmetric({ - workspaceId, - ciphertext, - iv, - tag - }: { - workspaceId: Types.ObjectId; - ciphertext: string; - iv: string; - tag: string; - }) { - return await decryptSymmetricHelper({ - workspaceId, - ciphertext, - iv, - tag - }); - } + /** + * Get workspace key for workspace with id [workspaceId] shared to bot. + * @param {Object} obj + * @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for + * @returns + */ + static async getWorkspaceKeyWithBot({ + workspaceId, + }: { + workspaceId: Types.ObjectId; + }) { + return await getKey({ + workspaceId, + }); + } + + /** + * Return decrypted secrets for workspace with id [workspaceId] and + * environment [environmen] shared to bot. + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace of secrets + * @param {String} obj.environment - environment for secrets + * @returns {Object} secretObj - object where keys are secret keys and values are secret values + */ + static async getSecrets({ + workspaceId, + environment, + secretPath, + }: { + workspaceId: Types.ObjectId; + environment: string; + secretPath: string; + }) { + return await getSecretsBotHelper({ + workspaceId, + environment, + secretPath, + }); + } + + /** + * Return symmetrically encrypted [plaintext] using the + * bot's copy of the workspace key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.plaintext - plaintext to encrypt + */ + static async encryptSymmetric({ + workspaceId, + plaintext, + }: { + workspaceId: Types.ObjectId; + plaintext: string; + }) { + return await encryptSymmetricHelper({ + workspaceId, + plaintext, + }); + } + + /** + * Return symmetrically decrypted [ciphertext] using the + * bot's copy of the workspace key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.ciphertext - ciphertext to decrypt + * @param {String} obj.iv - iv + * @param {String} obj.tag - tag + */ + static async decryptSymmetric({ + workspaceId, + ciphertext, + iv, + tag, + }: { + workspaceId: Types.ObjectId; + ciphertext: string; + iv: string; + tag: string; + }) { + return await decryptSymmetricHelper({ + workspaceId, + ciphertext, + iv, + tag, + }); + } } -export default BotService; \ No newline at end of file +export default BotService; diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index 4bb163aae1..af05cfab78 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -13,6 +13,7 @@ import { BackupPrivateKey, IntegrationAuth, ServiceTokenData, + Integration, } from "../../models"; import { generateKeyPair } from "../../utils/crypto"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; @@ -424,3 +425,19 @@ export const backfillServiceToken = async () => { ); console.log("Migration: Service token migration v1 complete"); }; + +export const backfillIntegration = async () => { + await Integration.updateMany( + { + secretPath: { + $exists: false, + }, + }, + { + $set: { + secretPath: "/", + }, + } + ); + console.log("Migration: Integration migration v1 complete"); +}; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index bec142e9e9..d7f333b17d 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -13,6 +13,7 @@ import { backfillEncryptionMetadata, backfillSecretFolders, backfillServiceToken, + backfillIntegration, } from "./backfillData"; import { reencryptBotPrivateKeys, @@ -77,6 +78,7 @@ export const setup = async () => { await backfillEncryptionMetadata(); await backfillSecretFolders(); await backfillServiceToken(); + await backfillIntegration(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY From a743c12c1bf6eac3311502e9bd9ab791a5ea99ce Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Thu, 15 Jun 2023 22:46:15 +0530 Subject: [PATCH 21/30] feat(folder-scoped-integrations): implemented ui for folders in integration page --- .../components/integrations/Integration.tsx | 68 ++++++---- .../integrations/IntegrationSection.tsx | 1 + .../api/integrations/createIntegration.ts | 55 ++++---- frontend/src/pages/integrations/[id].tsx | 1 + .../aws-parameter-store/create.tsx | 15 ++- .../aws-secret-manager/create.tsx | 11 +- .../integrations/azure-key-vault/create.tsx | 11 +- .../src/pages/integrations/checkly/create.tsx | 31 ++++- .../pages/integrations/circleci/create.tsx | 22 ++- .../src/pages/integrations/flyio/create.tsx | 22 ++- .../src/pages/integrations/github/create.tsx | 21 ++- .../src/pages/integrations/gitlab/create.tsx | 35 +++-- .../integrations/hashicorp-vault/create.tsx | 127 ++++++++++-------- .../src/pages/integrations/heroku/create.tsx | 21 ++- .../src/pages/integrations/netlify/create.tsx | 22 ++- .../src/pages/integrations/railway/create.tsx | 21 ++- .../src/pages/integrations/render/create.tsx | 22 ++- .../pages/integrations/supabase/create.tsx | 21 ++- .../pages/integrations/travisci/create.tsx | 22 ++- .../src/pages/integrations/vercel/create.tsx | 21 ++- 20 files changed, 417 insertions(+), 153 deletions(-) diff --git a/frontend/src/components/integrations/Integration.tsx b/frontend/src/components/integrations/Integration.tsx index 6a657c1a00..7bfd0d0c05 100644 --- a/frontend/src/components/integrations/Integration.tsx +++ b/frontend/src/components/integrations/Integration.tsx @@ -4,7 +4,11 @@ import { useRouter } from 'next/router'; import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; // TODO: This needs to be moved from public folder -import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants'; +import { + contextNetlifyMapping, + integrationSlugNameMapping, + reverseContextNetlifyMapping +} from 'public/data/frequentConstants'; import Button from '@app/components/basic/buttons/Button'; import ListBox from '@app/components/basic/Listbox'; @@ -27,6 +31,7 @@ interface Integration { targetEnvironment: string; workspace: string; integrationAuth: string; + secretPath: string; } interface IntegrationApp { @@ -55,7 +60,6 @@ const IntegrationTile = ({ environments = [], handleDeleteIntegration }: Props) => { - const [integrationEnvironment, setIntegrationEnvironment] = useState( environments.find(({ slug }) => slug === integration?.environment) || { name: '', @@ -74,16 +78,16 @@ const IntegrationTile = ({ }); setApps(tempApps); - + if (integration?.app) { setIntegrationApp(integration.app); } else if (integration?.path && integration?.region) { setIntegrationApp(`${integration.path} (${integration.region})`); } else if (tempApps.length > 0) { - setIntegrationApp(tempApps[0].name) - } else { - setIntegrationApp(''); - } + setIntegrationApp(tempApps[0].name); + } else { + setIntegrationApp(''); + } switch (integration.integration) { case 'vercel': @@ -212,12 +216,15 @@ const IntegrationTile = ({ return
; }; - if (!integrationApp && integration.integration !== "checkly") return
; - - const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp; + if (!integrationApp && integration.integration !== 'checkly') return
; + + const isSelected = + integration.integration === 'hashicorp-vault' + ? `${integration.app} - path: ${integration.path}` + : integrationApp; return ( -
+

ENVIRONMENT

@@ -235,33 +242,46 @@ const IntegrationTile = ({ isFull />
+
+

SECRET PATH

+
+ {/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */} + {integration.secretPath} +
+
-

INTEGRATION

-
+

INTEGRATION

+
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */} {integrationSlugNameMapping[integration.integration]}
APP
- {integrationApp ?
- app.name) : null} - isSelected={isSelected} - onChange={(app) => { - setIntegrationApp(app); - }} - /> -
:
-
} + {integrationApp ? ( +
+ app.name) : null} + isSelected={isSelected} + onChange={(app) => { + setIntegrationApp(app); + }} + /> +
+ ) : ( +
+ - +
+ )}
{renderIntegrationSpecificParams(integration)}
-
+
{integration.isActive ? ( -
+
In Sync
diff --git a/frontend/src/components/integrations/IntegrationSection.tsx b/frontend/src/components/integrations/IntegrationSection.tsx index 9b8799b6a5..cf1d7e9c46 100644 --- a/frontend/src/components/integrations/IntegrationSection.tsx +++ b/frontend/src/components/integrations/IntegrationSection.tsx @@ -23,6 +23,7 @@ interface Integration { targetEnvironment: string; workspace: string; integrationAuth: string; + secretPath: string; } const ProjectIntegrationSection = ({ diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts index e3e7c010a2..331adff51f 100644 --- a/frontend/src/pages/api/integrations/createIntegration.ts +++ b/frontend/src/pages/api/integrations/createIntegration.ts @@ -3,6 +3,7 @@ import SecurityClient from '@app/components/utilities/SecurityClient'; interface Props { integrationAuthId: string; isActive: boolean; + secretPath: string; app: string | null; appId: string | null; sourceEnvironment: string; @@ -20,19 +21,20 @@ interface Props { * @param {String} obj.accessToken - id of integration authorization for which to create the integration * @returns */ -const createIntegration = ({ - integrationAuthId, - isActive, - app, - appId, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region +const createIntegration = ({ + integrationAuthId, + isActive, + app, + appId, + sourceEnvironment, + targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, + owner, + path, + region, + secretPath }: Props) => SecurityClient.fetchCall('/api/v1/integration', { method: 'POST', @@ -40,18 +42,19 @@ const createIntegration = ({ 'Content-Type': 'application/json' }, body: JSON.stringify({ - integrationAuthId, - isActive, - app, - appId, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region + integrationAuthId, + isActive, + app, + appId, + sourceEnvironment, + targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, + owner, + path, + region, + secretPath }) }).then(async (res) => { if (res && res.status === 200) { @@ -61,4 +64,4 @@ const createIntegration = ({ return undefined; }); -export default createIntegration; \ No newline at end of file +export default createIntegration; diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx index 435c904f56..1f7050c42a 100644 --- a/frontend/src/pages/integrations/[id].tsx +++ b/frontend/src/pages/integrations/[id].tsx @@ -44,6 +44,7 @@ interface Integration { integration: string; targetEnvironment: string; workspace: string; + secretPath:string; integrationAuth: string; } diff --git a/frontend/src/pages/integrations/aws-parameter-store/create.tsx b/frontend/src/pages/integrations/aws-parameter-store/create.tsx index d06ac0061e..4f84ff4764 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/create.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/create.tsx @@ -56,6 +56,7 @@ export default function AWSParameterStoreCreateIntegrationPage() { const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); + const [secretPath, setSecretPath] = useState('/'); const [selectedAWSRegion, setSelectedAWSRegion] = useState(''); const [path, setPath] = useState(''); const [pathErrorText, setPathErrorText] = useState(''); @@ -69,9 +70,9 @@ export default function AWSParameterStoreCreateIntegrationPage() { } }, [workspace]); - const isValidAWSParameterStorePath = (secretPath: string) => { + const isValidAWSParameterStorePath = (awsStorePath: string) => { const pattern = /^\/([\w-]+\/)*[\w-]+\/$/; - return pattern.test(secretPath) && secretPath.length <= 2048; + return pattern.test(awsStorePath) && awsStorePath.length <= 2048; }; const handleButtonClick = async () => { @@ -101,7 +102,8 @@ export default function AWSParameterStoreCreateIntegrationPage() { targetServiceId: null, owner: null, path, - region: selectedAWSRegion + region: selectedAWSRegion, + secretPath }); setIsLoading(false); @@ -133,6 +135,13 @@ export default function AWSParameterStoreCreateIntegrationPage() { ))} + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + - - Checkly Integration + + + Checkly Integration + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setTargetEnvironment(e.target.value)} + + setTargetEnvironment(e.target.value)} /> diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx index 53f9b36668..639f774d77 100644 --- a/frontend/src/pages/integrations/heroku/create.tsx +++ b/frontend/src/pages/integrations/heroku/create.tsx @@ -2,7 +2,15 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import queryString from 'query-string'; -import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from '../../../components/v2'; import { useGetIntegrationAuthApps, useGetIntegrationAuthById @@ -23,6 +31,7 @@ export default function HerokuCreateIntegrationPage() { const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [targetApp, setTargetApp] = useState(''); + const [secretPath, setSecretPath] = useState('/'); const [isLoading, setIsLoading] = useState(false); @@ -60,7 +69,8 @@ export default function HerokuCreateIntegrationPage() { targetServiceId: null, owner: null, path: null, - region: null + region: null, + secretPath }); setIsLoading(false); @@ -94,6 +104,13 @@ export default function HerokuCreateIntegrationPage() { ))} + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> +