diff --git a/.env.example b/.env.example index 989a285e33..ec832f9e9d 100644 --- a/.env.example +++ b/.env.example @@ -9,13 +9,13 @@ ENCRYPTION_KEY=replace_with_lengthy_secure_hex JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex JWT_AUTH_SECRET=replace_with_lengthy_secure_hex +JWT_SERVICE_SECRET=replace_with_lengthy_secure_hex # JWT lifetime # Optional lifetimes for JWT tokens expressed in seconds or a string # describing a time span (e.g. 60, "2 days", "10h", "7d") JWT_AUTH_LIFETIME= JWT_REFRESH_LIFETIME= -JWT_SERVICE_SECRET= JWT_SIGNUP_LIFETIME= # Optional lifetimes for OTP expressed in seconds diff --git a/.github/workflows/release_docker_k8_operator.yaml b/.github/workflows/release_docker_k8_operator.yaml index 01aa3b6250..788d414b6e 100644 --- a/.github/workflows/release_docker_k8_operator.yaml +++ b/.github/workflows/release_docker_k8_operator.yaml @@ -26,13 +26,4 @@ jobs: context: k8-operator push: true platforms: linux/amd64,linux/arm64 - tags: infisical/kubernetes-operator:latest - - - uses: actions/setup-go@v2 - - - name: Upload CRD manifest - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: dist/install-secrets-operator.yaml - tag: ${{ github.ref }} \ No newline at end of file + tags: infisical/kubernetes-operator:latest \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e839733970..9ac38524bc 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -81,6 +81,7 @@ nfpms: - rpm - deb - apk + - archlinux bindir: /usr/bin scoop: bucket: diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index fa81e63deb..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": true, - "trailingComma": "none", - "singleQuote": true, - "printWidth": 80, - "useTabs": false -} diff --git a/README.md b/README.md index ba76997f1f..caf79191e6 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ Slack community channel + + Infisical Twitter + Dashboard @@ -92,12 +95,6 @@ Not sure where to get started? You can: We're currently in Public Alpha. -## 🚨 Stay Up-to-Date - -Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates: - -![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true) - ## 🔌 Integrations We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :) @@ -131,10 +128,14 @@ We're currently setting the foundation and building [integrations](https://infis - 🔜 Vercel (https://github.com/Infisical/infisical/issues/60) + + ✔️ Vercel + - 🔜 GitLab CI/CD + + ✔️ Kubernetes + 🔜 Fly.io @@ -156,10 +157,10 @@ We're currently setting the foundation and building [integrations](https://infis 🔜 GCP - 🔜 Kubernetes + 🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134) - 🔜 CircleCI + 🔜 CircleCI (https://github.com/Infisical/infisical/issues/91) @@ -192,7 +193,7 @@ We're currently setting the foundation and building [integrations](https://infis 🔜 Supabase - 🔜 Serverless + 🔜 Render (https://github.com/Infisical/infisical/issues/132) @@ -302,6 +303,12 @@ This repo is entirely MIT licensed, with the exception of the `ee` directory whi Looking to report a security vulnerability? Please don't post about it in GitHub issue. Instead, refer to our [SECURITY.md](./SECURITY.md) file. +## 🚨 Stay Up-to-Date + +Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates: + +![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true) + ## 🦸 Contributors [//]: contributor-faces @@ -310,4 +317,4 @@ Looking to report a security vulnerability? Please don't post about it in GitHub - + diff --git a/backend/.eslintrc b/backend/.eslintrc index 7fe1989012..c1ca1a1eba 100644 --- a/backend/.eslintrc +++ b/backend/.eslintrc @@ -1,11 +1,10 @@ { "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "prettier"], + "plugins": ["@typescript-eslint"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier" + "plugin:@typescript-eslint/recommended" ], "rules": { "no-console": 2 diff --git a/backend/Dockerfile b/backend/Dockerfile index ccc76e66e5..85b7204fec 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,12 +4,12 @@ WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --only-production +RUN npm ci --only-production --ignore-scripts COPY . . HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ - CMD node healthcheck.js + CMD node healthcheck.js CMD ["npm", "run", "start"] diff --git a/backend/package-lock.json b/backend/package-lock.json index 8330bfefc2..693f055a65 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -52,13 +52,10 @@ "@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/parser": "^5.40.1", "eslint": "^8.26.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.2.1", "install": "^0.13.0", "jest": "^29.3.1", "nodemon": "^2.0.19", "npm": "^8.19.3", - "prettier": "^2.7.1", "ts-node": "^10.9.1" } }, @@ -3073,12 +3070,6 @@ "@types/node": "*" } }, - "node_modules/@types/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", - "dev": true - }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -3930,6 +3921,7 @@ "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", + "fsevents": "~2.3.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -4431,39 +4423,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-prettier": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", - "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -4781,12 +4740,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, "node_modules/fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -5180,6 +5133,7 @@ "minimist": "^1.2.5", "neo-async": "^2.6.0", "source-map": "^0.6.1", + "uglify-js": "^3.1.4", "wordwrap": "^1.0.0" }, "bin": { @@ -5837,6 +5791,7 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.2.0", "jest-util": "^29.3.1", @@ -6053,7 +6008,6 @@ "@jest/transform": "^29.3.1", "@jest/types": "^29.3.1", "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.3.1", @@ -6641,9 +6595,11 @@ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.11.0.tgz", "integrity": "sha512-9l9n4Nk2BYZzljW3vHah3Z0rfS5npKw6ktnkmFgTcnzaXH1DRm3pDl6VMHu84EVb1lzmSaJC4OzWZqTkB5i2wg==", "dependencies": { + "@aws-sdk/credential-providers": "^3.186.0", "bson": "^4.7.0", "denque": "^2.1.0", "mongodb-connection-string-url": "^2.5.4", + "saslprep": "^1.0.3", "socks": "^2.7.1" }, "engines": { @@ -6873,9 +6829,6 @@ }, "bin": { "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" } }, "node_modules/normalize-path": { @@ -7729,6 +7682,7 @@ "inBundle": true, "license": "MIT", "dependencies": { + "@colors/colors": "1.5.0", "string-width": "^4.2.0" }, "engines": { @@ -8524,6 +8478,7 @@ "inBundle": true, "license": "MIT", "dependencies": { + "encoding": "^0.1.13", "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" @@ -9897,33 +9852,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/pretty-format": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", @@ -13854,12 +13782,6 @@ "@types/node": "*" } }, - "@types/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", - "dev": true - }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -14860,22 +14782,6 @@ } } }, - "eslint-config-prettier": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", - "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -15098,12 +15004,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, "fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -16033,7 +15933,6 @@ "@jest/transform": "^29.3.1", "@jest/types": "^29.3.1", "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.3.1", @@ -18718,21 +18617,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, "pretty-format": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", diff --git a/backend/package.json b/backend/package.json index a2c5830480..119a79073a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,12 +34,12 @@ "version": "1.0.0", "main": "src/index.js", "scripts": { + "prepare": "cd .. && npm install", "start": "npm run build && node build/index.js", "dev": "nodemon", - "build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build", + "build": "rimraf ./build && tsc && cp -R ./src/templates ./build", "lint": "eslint . --ext .ts", "lint-and-fix": "eslint . --ext .ts --fix", - "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write", "lint-staged": "lint-staged" }, "repository": { @@ -66,13 +66,10 @@ "@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/parser": "^5.40.1", "eslint": "^8.26.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.2.1", "install": "^0.13.0", "jest": "^29.3.1", "nodemon": "^2.0.19", "npm": "^8.19.3", - "prettier": "^2.7.1", "ts-node": "^10.9.1" } } diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index aa02d08a18..99c72ab3d4 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -18,9 +18,8 @@ const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB! || 'e787fc24bcec43ecd5d5'; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; -const CLIENT_SECRET_GITHUB = - process.env.CLIENT_SECRET_GITHUB! || - '407f32da788f63559abd662c6de08bb2911ca8ae'; +const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB! || '407f32da788f63559abd662c6de08bb2911ca8ae'; +const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY! || @@ -63,6 +62,7 @@ export { CLIENT_SECRET_VERCEL, CLIENT_SECRET_NETLIFY, CLIENT_SECRET_GITHUB, + CLIENT_SLUG_VERCEL, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, PRIVATE_KEY, diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 9aaff9741a..ccfd72a535 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -53,12 +53,12 @@ const handleOAuthExchangeHelper = async ({ if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange'); // exchange code for access and refresh tokens - let res = await exchangeCode({ + const res = await exchangeCode({ integration, code }); - let update: Update = { + const update: Update = { workspace: workspaceId, integration } @@ -138,7 +138,7 @@ const syncIntegrationsHelper = async ({ // to that integration for await (const integration of integrations) { // get workspace, environment (shared) secrets - const secrets = await BotService.getSecrets({ + const secrets = await BotService.getSecrets({ // issue here? workspaceId: integration.workspace.toString(), environment: integration.environment }); diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 363e96f34d..9c6b66674c 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -64,14 +64,13 @@ const syncSecrets = async ({ }); break; case INTEGRATION_GITHUB: - await syncSecretsNetlify({ + await syncSecretsGitHub({ integration, integrationAuth, secrets, accessToken }); break; - } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -137,157 +136,149 @@ const syncSecretsHeroku = async ({ * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) */ const syncSecretsVercel = async ({ - integration, - secrets, - accessToken + integration, + secrets, + accessToken }: { - integration: IIntegration; - secrets: any; - accessToken: string; + integration: IIntegration, + secrets: any; + accessToken: string; }) => { - interface VercelSecret { - id?: string; - type: string; - key: string; - value: string; - target: string[]; - } - - try { - // Get all (decrypted) secrets back from Vercel in - // decrypted format - const params = new URLSearchParams({ - decrypt: 'true' - }); - - const res = ( - await Promise.all( - ( - await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + + interface VercelSecret { + id?: string; + type: string; + key: string; + value: string; + target: string[]; + } + + try { + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params = new URLSearchParams({ + decrypt: "true" + }); + + const res = (await Promise.all((await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } + params, + headers: { + Authorization: `Bearer ${accessToken}` + } } - ) - ).data.envs - .filter((secret: VercelSecret) => - secret.target.includes(integration.target) - ) - .map( - async (secret: VercelSecret) => - ( - await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { + )) + .data + .envs + .filter((secret: VercelSecret) => secret.target.includes(integration.target)) + .map(async (secret: VercelSecret) => (await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { headers: { - Authorization: `Bearer ${accessToken}` + Authorization: `Bearer ${accessToken}` } - } - ) - ).data - ) - ) - ).reduce( - (obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), - {} - ); - - const updateSecrets: VercelSecret[] = []; - const deleteSecrets: VercelSecret[] = []; - const newSecrets: VercelSecret[] = []; - - // Identify secrets to create - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.target] + + } + )).data) + )).reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; + + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key: key, + value: secrets[key], + type: 'encrypted', + target: [integration.target] + }); + } }); - } - }); - - // Identify secrets to update and delete - Object.keys(res).map((key) => { - if (key in secrets) { - if (res[key].value !== secrets[key]) { - // case: secret value has changed - updateSecrets.push({ - id: res[key].id, - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.target] - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - id: res[key].id, - key: key, - value: res[key].value, - type: 'encrypted', - target: [integration.target] + + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: 'encrypted', + target: [integration.target] + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: 'encrypted', + target: [integration.target], + }); + } }); - } - }); - // Sync/push new secrets - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, - newSecrets, - { - headers: { - Authorization: `Bearer ${accessToken}` - } + // Sync/push new secrets + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); } - ); - } - // Sync/push updated secrets - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: VercelSecret) => { - const { id, ...updatedSecret } = secret; - await axios.patch( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - updatedSecret, - { - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } + // Sync/push updated secrets + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: VercelSecret) => { + const { + id, + ...updatedSecret + } = secret; + await axios.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + updatedSecret, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } - // Delete secrets - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (secret: VercelSecret) => { - await axios.delete( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); + // Delete secrets + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (secret: VercelSecret) => { + await axios.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Vercel'); } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Vercel'); - } -}; +} /** * Sync/push [secrets] to Netlify site [app] @@ -297,165 +288,211 @@ const syncSecretsVercel = async ({ * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) */ const syncSecretsNetlify = async ({ - integration, - integrationAuth, - secrets, - accessToken + integration, + integrationAuth, + secrets, + accessToken }: { - integration: IIntegration; - integrationAuth: IIntegrationAuth; - secrets: any; - accessToken: string; + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; }) => { - try { - const getParams = new URLSearchParams({ - context_name: integration.context, - site_id: integration.siteId - }); + try { - const res = ( - await axios.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - { - params: getParams, - headers: { - Authorization: `Bearer ${accessToken}` - } + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; } - ) - ).data.reduce( - (obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret.values[0].value - }), - {} - ); - - interface UpdateNetlifySecret { - key: string; - context: string; - value: string; - } - - interface DeleteNetlifySecret { - key: string; - } - - interface NewNetlifySecretValue { - value: string; - context: string; - } - - interface NewNetlifySecret { - key: string; - values: NewNetlifySecretValue[]; - } - - const updateSecrets: UpdateNetlifySecret[] = []; - const deleteSecrets: DeleteNetlifySecret[] = []; - const newSecrets: NewNetlifySecret[] = []; - - // Identify secrets to create - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key: key, - values: [ + + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } + + interface NetlifySecretsRes { + [index: string]: NetlifySecret; + } + + const getParams = new URLSearchParams({ + context_name: 'all', // integration.context or all + site_id: integration.siteId + }); + + const res = (await axios.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, { - value: secrets[key], // include id? - context: integration.context + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + )) + .data + .reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [{ + value: secrets[key], + context: integration.context + }] + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values + .reduce((obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value + }), {}); + + if (integration.context in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.context].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value + updateSecrets.push({ + key, + values: [{ + context: integration.context, + value: secrets[key] + }] + }); + } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [{ + context: integration.context, + value: secrets[key] + }] + }); + } + } + }) + + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).map((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical + + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.context) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [{ + id: value.id, + context: integration.context, + value: value.value + }] + }); + } + } + }); } - ] }); - } - }); - // Identify secrets to update and delete - Object.keys(res).map((key) => { - if (key in secrets) { - if (res[key] !== secrets[key]) { - // case: secret value has changed - updateSecrets.push({ - key: key, - context: integration.context, - value: secrets[key] - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - key + const syncParams = new URLSearchParams({ + site_id: integration.siteId }); - } - }); - const syncParams = new URLSearchParams({ - site_id: integration.siteId - }); + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + } - // Sync/push new secrets - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - newSecrets, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await axios.patch( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value + }, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); } - ); - } - // Sync/push updated secrets - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: UpdateNetlifySecret) => { - await axios.patch( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, - { - context: secret.context, - value: secret.value - }, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } - // Delete secrets - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Heroku'); } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); - } -}; +} /** - * Sync/push [secrets] to Github site [app] + * Sync/push [secrets] to GitHub site [app] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) */ -const syncSecretsGithub = async ({ +const syncSecretsGitHub = async ({ integration, integrationAuth, secrets, diff --git a/backend/src/routes/password.ts b/backend/src/routes/password.ts index 955e532a0b..8032cba83d 100644 --- a/backend/src/routes/password.ts +++ b/backend/src/routes/password.ts @@ -27,7 +27,6 @@ router.post( passwordController.changePassword ); -// NEW router.post( '/email/password-reset', passwordLimiter, @@ -36,7 +35,6 @@ router.post( passwordController.emailPasswordReset ); -// NEW router.post( '/email/password-reset-verify', passwordLimiter, @@ -46,7 +44,6 @@ router.post( passwordController.emailPasswordResetVerify ); -// NEW router.get( '/backup-private-key', passwordLimiter, @@ -68,7 +65,6 @@ router.post( passwordController.createBackupPrivateKey ); -// NEW router.post( '/password-reset', requireSignupAuth, diff --git a/backend/src/utils/crypto.ts b/backend/src/utils/crypto.ts index 585dae51d2..28f96b0cfb 100644 --- a/backend/src/utils/crypto.ts +++ b/backend/src/utils/crypto.ts @@ -1,6 +1,7 @@ import nacl from 'tweetnacl'; import util from 'tweetnacl-util'; import AesGCM from './aes-gcm'; +import * as Sentry from '@sentry/node'; /** * Return new base64, NaCl, public-private key pair. @@ -47,6 +48,8 @@ const encryptAsymmetric = ({ util.decodeBase64(privateKey) ); } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); throw new Error('Failed to perform asymmetric encryption'); } @@ -86,6 +89,8 @@ const decryptAsymmetric = ({ util.decodeBase64(privateKey) ); } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); throw new Error('Failed to perform asymmetric decryption'); } @@ -112,6 +117,8 @@ const encryptSymmetric = ({ iv = obj.iv; tag = obj.tag; } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); throw new Error('Failed to perform symmetric encryption'); } @@ -147,6 +154,8 @@ const decryptSymmetric = ({ try { plaintext = AesGCM.decrypt(ciphertext, iv, tag, key); } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); throw new Error('Failed to perform symmetric decryption'); } diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index f67b57b59f..2e0a8182b6 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,6 +1,7 @@ import { CLIENT_ID_HEROKU, - CLIENT_ID_NETLIFY + CLIENT_ID_NETLIFY, + CLIENT_SLUG_VERCEL } from '../config'; // integrations @@ -49,6 +50,7 @@ const INTEGRATION_OPTIONS = [ isAvailable: true, type: 'vercel', clientId: '', + clientSlug: CLIENT_SLUG_VERCEL, docsLink: '' }, { diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index ec5210b1fd..7518fe98df 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "os/signal" + "runtime" "strings" "syscall" @@ -19,12 +20,38 @@ import ( // runCmd represents the run command var runCmd = &cobra.Command{ + Example: ` + infisical run --env=dev -- npm run dev + infisical run --command "first-command && second-command; more-commands..." + `, Use: "run [any infisical run command flags] -- [your application start command]", Short: "Used to inject environments variables into your application process", DisableFlagsInUseLine: true, - Example: "infisical run --env=prod -- npm run dev", - Args: cobra.MinimumNArgs(1), PreRun: toggleDebug, + Args: func(cmd *cobra.Command, args []string) error { + // Check if the --command flag has been set + commandFlagSet := cmd.Flags().Changed("command") + + // If the --command flag has been set, check if a value was provided + if commandFlagSet { + command := cmd.Flag("command").Value.String() + if command == "" { + return fmt.Errorf("you need to provide a command after the flag --command") + } + + // If the --command flag has been set, args should not be provided + if len(args) > 0 { + return fmt.Errorf("you cannot set any arguments after --command flag. --command only takes a string command") + } + } else { + // If the --command flag has not been set, at least one arg should be provided + if len(args) == 0 { + return fmt.Errorf("at least one argument is required after the run command, received %d", len(args)) + } + } + + return nil + }, Run: func(cmd *cobra.Command, args []string) { envName, err := cmd.Flags().GetString("env") if err != nil { @@ -54,10 +81,23 @@ var runCmd = &cobra.Command{ } if shouldExpandSecrets { - secretsWithSubstitutions := util.SubstituteSecrets(secrets) - execCmd(args[0], args[1:], secretsWithSubstitutions) + secrets = util.SubstituteSecrets(secrets) + } + + if cmd.Flags().Changed("command") { + command := cmd.Flag("command").Value.String() + err = executeMultipleCommandWithEnvs(command, secrets) + if err != nil { + log.Errorf("Something went wrong when executing your command [error=%s]", err) + return + } } else { - execCmd(args[0], args[1:], secrets) + err = executeSingleCommandWithEnvs(args, secrets) + if err != nil { + log.Errorf("Something went wrong when executing your command [error=%s]", err) + return + } + return } }, @@ -68,22 +108,51 @@ func init() { runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") + runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") } -// Credit: inspired by AWS Valut -func execCmd(command string, args []string, envs []models.SingleEnvironmentVariable) error { - numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(envs)) - +// Will execute a single command and pass in the given secrets into the process +func executeSingleCommandWithEnvs(args []string, secrets []models.SingleEnvironmentVariable) error { + command := args[0] + argsForCommand := args[1:] + numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets)) log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) - log.Debugf("executing command: %s %s \n", command, strings.Join(args, " ")) - log.Debugln("Secrets injected:", envs) + log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " ")) + log.Debugln("Secrets injected:", secrets) + + cmd := exec.Command(command, argsForCommand...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = getAllEnvs(secrets) + + return execCmd(cmd) +} + +func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleEnvironmentVariable) error { + shell := [2]string{"sh", "-c"} + if runtime.GOOS == "windows" { + shell = [2]string{"cmd", "/C"} + } else { + shell[0] = os.Getenv("SHELL") + } - cmd := exec.Command(command, args...) + cmd := exec.Command(shell[0], shell[1], fullCommand) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Env = getAllEnvs(envs) + cmd.Env = getAllEnvs(secrets) + numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets)) + log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) + log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand) + log.Debugln("Secrets injected:", secrets) + + return execCmd(cmd) +} + +// Credit: inspired by AWS Valut +func execCmd(cmd *exec.Cmd) error { sigChannel := make(chan os.Signal, 1) signal.Notify(sigChannel) @@ -100,7 +169,7 @@ func execCmd(command string, args []string, envs []models.SingleEnvironmentVaria if err := cmd.Wait(); err != nil { _ = cmd.Process.Signal(os.Kill) - return fmt.Errorf("Failed to wait for command termination: %v", err) + return fmt.Errorf("failed to wait for command termination: %v", err) } waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) diff --git a/docs/cli/commands/export.mdx b/docs/cli/commands/export.mdx index fd58868ff8..b80fb74703 100644 --- a/docs/cli/commands/export.mdx +++ b/docs/cli/commands/export.mdx @@ -30,4 +30,7 @@ infisical export --format=csv > secrets.csv # Export variables to a JSON file infisical export --format=json > secrets.json + +# Export variables to a YAML file +infisical export --format=yaml > secrets.yaml ``` diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index 7fb2076129..2c65ef53e9 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -2,9 +2,25 @@ title: "infisical run" --- -```bash -infisical run [options] -- [your application start command] -``` + + + ```bash + infisical run [options] -- [your application start command] + + # Example + infisical run [options] -- npm run dev + ``` + + + + ```bash + infisical run [options] --command [string command] + + # Example + infisical run [options] --command "npm run bootstrap && npm run dev start; other-bash-command" + ``` + + ## Description @@ -15,5 +31,6 @@ Inject environment variables from the platform into an application process. | Option | Description | Default value | | -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | | `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | -| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` | +| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | None | | `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | +| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None | diff --git a/docs/contributing/developing.mdx b/docs/contributing/developing.mdx index 3be1a1b6bf..25a1f72ee1 100644 --- a/docs/contributing/developing.mdx +++ b/docs/contributing/developing.mdx @@ -23,7 +23,7 @@ Mandatory variables in the `.env` file: 1. Keys and JWT variables -![image](https://user-images.githubusercontent.com/118568289/206791534-9c9d1431-e83d-49c0-8a54-b373ed0df820.png) +![image](https://user-images.githubusercontent.com/8071263/208800914-f468c2ad-c6a8-4da7-8ffd-eece5d7f08d2.png) The `.env.example` has these variables empty, you can self generate the `JWT and ENCRYPTION_KEY` with this [32-byte random hex strings generator](https://www.browserling.com/tools/random-hex). diff --git a/docs/images/integrations-heroku-auth.png b/docs/images/integrations-heroku-auth.png new file mode 100644 index 0000000000..da9b4cf529 Binary files /dev/null and b/docs/images/integrations-heroku-auth.png differ diff --git a/docs/images/integrations-heroku.png b/docs/images/integrations-heroku.png new file mode 100644 index 0000000000..cc225f2860 Binary files /dev/null and b/docs/images/integrations-heroku.png differ diff --git a/docs/images/integrations-netlify-auth.png b/docs/images/integrations-netlify-auth.png new file mode 100644 index 0000000000..fe25d7acf2 Binary files /dev/null and b/docs/images/integrations-netlify-auth.png differ diff --git a/docs/images/integrations-netlify.png b/docs/images/integrations-netlify.png new file mode 100644 index 0000000000..60261043e3 Binary files /dev/null and b/docs/images/integrations-netlify.png differ diff --git a/docs/images/integrations-vercel-auth.png b/docs/images/integrations-vercel-auth.png new file mode 100644 index 0000000000..d8f3d2d185 Binary files /dev/null and b/docs/images/integrations-vercel-auth.png differ diff --git a/docs/images/integrations-vercel.png b/docs/images/integrations-vercel.png new file mode 100644 index 0000000000..f3a814c7a3 Binary files /dev/null and b/docs/images/integrations-vercel.png differ diff --git a/docs/images/integrations.png b/docs/images/integrations.png new file mode 100644 index 0000000000..88b0c7aec9 Binary files /dev/null and b/docs/images/integrations.png differ diff --git a/docs/integrations/cicd/circleci.mdx b/docs/integrations/cicd/circleci.mdx new file mode 100644 index 0000000000..7ded52d8aa --- /dev/null +++ b/docs/integrations/cicd/circleci.mdx @@ -0,0 +1,5 @@ +--- +title: "Circle CI" +--- + +Coming soon. diff --git a/docs/integrations/cloud/flyio.mdx b/docs/integrations/cloud/flyio.mdx new file mode 100644 index 0000000000..b53a524041 --- /dev/null +++ b/docs/integrations/cloud/flyio.mdx @@ -0,0 +1,5 @@ +--- +title: "Fly.io" +--- + +Coming soon. diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx index 5f0debd3e4..e16cd0d540 100644 --- a/docs/integrations/cloud/heroku.mdx +++ b/docs/integrations/cloud/heroku.mdx @@ -1,26 +1,29 @@ --- title: "Heroku" -description: "With this integration, you can automatically sync your secrets to Heroku as soon as you update secrets in Infisical." --- -## Instructions +Prerequisites: -### Step 1: Open the integrations console +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -Open the Infisical Dashboard. Choose the project in which you want to set up the intergation. Go to the integrations tab in the left sidebar. +## Navigate to your project's integrations tab -### Step 2: Authenticate with Heroku +![integrations](../../images/integrations.png) -Click on "Heroku" tile. Log in if required and provide the necessary permissions to Infisical. You will afterwards be redirected back to the integrations page. +## Authorize Infisical for Heroku -Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. With any questions, reach out support@infisical.com. +Press on the Heroku tile and grant Infisical access to your Heroku account. -### Step 3: Start integration +![integrations heroku authorization](../../images/integrations-heroku-auth.png) -Choose a Heroku App that you want to sync the secrets to, and the Infisical project environment that you want to sync the secrets from. Start the integration. + + If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables. + Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform. + -The integration should now show status 'In Sync'. Every time you edit secrets, they will be automatically pushed to Heroku. +## Start integration + +Select which Infisical environment secrets you want to sync to which Heroku app and press start integration to start syncing secrets to Heroku. + +![integrations heroku](../../images/integrations-heroku.png) - - If you need to update your integration, you will have to delete the current one and create a new one. - diff --git a/docs/integrations/cloud/netlify.mdx b/docs/integrations/cloud/netlify.mdx new file mode 100644 index 0000000000..e78f013686 --- /dev/null +++ b/docs/integrations/cloud/netlify.mdx @@ -0,0 +1,32 @@ +--- +title: "Netlify" +--- + + + Infisical integrates with Netlify's new environment variable experience. If your site uses Netlify's old environment variable experience, you'll have to upgrade it to the new one to use this integration. + + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for Netlify + +Press on the Netlify tile and grant Infisical access to your Netlify account. + +![integrations netlify authorization](../../images/integrations-netlify-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables. + Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which Netlify app and context. Lastly, press start integration to start syncing secrets to Netlify. + +![integrations netlify](../../images/integrations-netlify.png) \ No newline at end of file diff --git a/docs/integrations/cloud/render.mdx b/docs/integrations/cloud/render.mdx new file mode 100644 index 0000000000..895bf01d17 --- /dev/null +++ b/docs/integrations/cloud/render.mdx @@ -0,0 +1,5 @@ +--- +title: "Render" +--- + +Coming soon. diff --git a/docs/integrations/cloud/vercel.mdx b/docs/integrations/cloud/vercel.mdx index eb09203b57..59b416c446 100644 --- a/docs/integrations/cloud/vercel.mdx +++ b/docs/integrations/cloud/vercel.mdx @@ -2,4 +2,22 @@ title: "Vercel" --- -Coming soon. +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for Vercel + +Press on the Vercel tile and grant Infisical access to your Vercel account. + +![integrations vercel authorization](../../images/integrations-vercel-auth.png) + +## Start integration + +Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press start integration to start syncing secrets to Vercel. + +![integrations vercel](../../images/integrations-vercel.png) \ No newline at end of file diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 9323f8917e..fb8a5d6314 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Overview" +title: 'Overview' --- Integrations allow environment variables to be synced from Infisical into your local development workflow, CI/CD pipelines, and production infrastructure. @@ -10,18 +10,10 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | -------------------------------------------------------- | --------- | ----------- | | [Docker](/integrations/platforms/docker) | Platform | Available | | [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available | -| Kubernetes | Platform | Coming soon | +| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available | | [Heroku](/integrations/cloud/heroku) | Cloud | Available | -| [Vercel](/integrations/cloud/vercel) | Cloud | Coming soon | -| AWS | Cloud | Coming soon | -| GCP | Cloud | Coming soon | -| Azure | Cloud | Coming soon | -| DigitalOcean | Cloud | Coming soon | -| GitLab | CI/CD | Coming soon | -| CircleCI | CI/CD | Coming soon | -| TravisCI | CI/CD | Coming soon | -| GitHub Actions | CI/CD | Coming soon | -| Jenkins | CI/CD | Coming soon | +| [Vercel](/integrations/cloud/vercel) | Cloud | Available | +| [Netlify](/integrations/cloud/netlify) | Cloud | Available | | [React](/integrations/frameworks/react) | Framework | Available | | [Vue](/integrations/frameworks/vue) | Framework | Available | | [Express](/integrations/frameworks/express) | Framework | Available | @@ -36,3 +28,14 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | [Flask](/integrations/frameworks/flask) | Framework | Available | | [Laravel](/integrations/frameworks/laravel) | Framework | Available | | [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | +| [Render](/integrations/cloud/render) | Cloud | Coming soon | +| [Fly.io](/integrations/cloud/flyio) | Cloud | Coming soon | +| AWS | Cloud | Coming soon | +| GCP | Cloud | Coming soon | +| Azure | Cloud | Coming soon | +| DigitalOcean | Cloud | Coming soon | +| GitLab | CI/CD | Coming soon | +| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon | +| TravisCI | CI/CD | Coming soon | +| GitHub Actions | CI/CD | Coming soon | +| Jenkins | CI/CD | Coming soon | diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx new file mode 100644 index 0000000000..51a9d7ec4f --- /dev/null +++ b/docs/integrations/platforms/kubernetes.mdx @@ -0,0 +1,161 @@ +--- +title: 'Kubernetes' +--- + +The Infisical Secrets Operator is a custom Kubernetes controller that helps keep secrets in a cluster up to date by synchronizing them. +It is installed in its own namespace within the cluster and follows strict RBAC policies. +The operator uses InfisicalSecret custom resources to identify which secrets to sync and where to store them. +It is responsible for continuously updating managed secrets, and in the future may also automatically reload deployments that use them as needed. + +## Install Operator + +The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) + + + + Install Infisical Helm repository + ```bash + helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + + helm repo update + ``` + + Install the Helm chart + ```bash + helm install --generate-name infisical-helm-charts/secrets-operator + ``` + + + + The operator will be installed in `infisical-operator-system` namespace + ``` + kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml + ``` + + + +## Sync Infisical Secrets to your cluster + +To retrieve secrets from an Infisical project and store them in your Kubernetes cluster, you can use the InfisicalSecret custom resource. +This resource is available after installing the Infisical operator. In order to specify the Infisical Token location and the location where the retrieved secrets should be stored, you can use the `tokenSecretReference` and `managedSecretReference` fields within the InfisicalSecret resource. + + + The `tokenSecretReference` field in the InfisicalSecret resource is used to specify the location of the Infisical Token, which is required for authenticating and retrieving secrets from an Infisical project. + + To create a Kubernetes secret containing an [Infisical Token](../../getting-started/dashboard/token), you can run the following command. + ``` bash + kubectl create secret generic service-token --from-literal=infisicalToken= + ``` + +Once the secret is created, add the name and namespace of the secret under `tokenSecretReference` field in the InfisicalSecret custom resource. + +{' '} + + + No matter what the name of the secret is or its namespace, it must contain a + key named `infisicalToken` with a valid Infisical Token as the value + + + + + +The `managedSecretReference` field in the InfisicalSecret resource is used to specify the location where secrets retrieved from an Infisical project should be stored. +You should specify the name and namespace of the Kubernetes secret that will hold these secrets. The operator will create the secret for you, you just need to provide its name and namespace. + +It is recommended that the managed secret be created in the same namespace as the deployment that will use it. + + + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + # Name of of this InfisicalSecret resource + name: infisicalsecret-sample +spec: + # The host that should be used to pull secrets from. The default value is https://infisical.com/api. + hostAPI: https://infisical.com/api + + # The Infisical project from which to pull secrets from + projectId: 62faf98ae0b05e8529b5da46 + + # The environment (dev, prod, testing, etc.) of the above project from where secrets should be pulled from + environment: dev + + # The Kubernetes secret the stores the Infisical token + tokenSecretReference: + # Kubernetes secret name + secretName: service-token + # The secret namespace + secretNamespace: default + + # The Kubernetes secret that Infisical Operator will create and populate with secrets from the above project + managedSecretReference: + # The name of managed Kubernetes secret that should be created + secretName: managed-secret + # The namespace the managed secret should be installed in + secretNamespace: default +``` + +## Verify + +To use the InfisicalSecret custom resource in your deployment, you can simply reference the managed secret specified in the `managedSecretReference` field as you would any other Kubernetes secret. +To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified. + +```bash +# Verify managed secret is created +kubectl get secrets -n +``` + + + The Infisical secrets will be synced and stored into the managed secret every + 5 minutes. + + +## Troubleshoot + +If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret. +It will continue attempting to reconnect to the API indefinitely. +The InfisicalSecret resource uses the `status.conditions` field to report its current state and any errors encountered. + +```yaml +$ kubectl get infisicalSecrets +NAME AGE +infisicalsecret-sample 12s + +$ kubectl describe infisicalSecret infisicalsecret-sample +... +Spec: +... +Status: + Conditions: + Last Transition Time: 2022-12-18T04:29:09Z + Message: Infisical controller has located the Infisical token in provided Kubernetes secret + Reason: OK + Status: True + Type: secrets.infisical.com/LoadedInfisicalToken + Last Transition Time: 2022-12-18T04:29:10Z + Message: Failed to update secret because: 400 Bad Request + Reason: Error + Status: False + Type: secrets.infisical.com/ReadyToSyncSecrets +Events: +``` + +## Uninstall Operator + +The managed secret created by the operator will not be deleted when the operator is uninstalled. + + + + Install Infisical Helm repository + ```bash + helm uninstall add + ``` + + + ``` + kubectl delete -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 547291fa5e..ff00ca83a1 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -21,7 +21,9 @@ "to": "#F8B7BD" } }, - "topbarLinks": [{ "name": "Log In", "url": "https://app.infisical.com/login" }], + "topbarLinks": [ + { "name": "Log In", "url": "https://app.infisical.com/login" } + ], "topbarCtaButton": { "name": "Start for Free", "url": "https://app.infisical.com/signup" @@ -113,27 +115,33 @@ "pages": ["self-hosting/configuration/envars"] } ] - }, + }, { "group": "Integrations", - "pages": [ - "integrations/overview" - ] + "pages": ["integrations/overview"] }, { "group": "Platforms", "pages": [ "integrations/platforms/docker", - "integrations/platforms/docker-compose" + "integrations/platforms/docker-compose", + "integrations/platforms/kubernetes" ] }, { "group": "Cloud", "pages": [ "integrations/cloud/heroku", - "integrations/cloud/vercel" + "integrations/cloud/vercel", + "integrations/cloud/netlify", + "integrations/cloud/render", + "integrations/cloud/flyio" ] }, + { + "group": "CI/CD", + "pages": ["integrations/cicd/circleci"] + }, { "group": "Frameworks", "pages": [ @@ -171,5 +179,8 @@ ] } ], - "backgroundImage": "/images/background.png" + "backgroundImage": "/images/background.png", + "integrations": { + "intercom": "hsg644ru" + } } diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 5deb468f74..520f0fb7fc 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -10,7 +10,7 @@ WORKDIR /app COPY package.json package-lock.json next.config.js ./ # Install dependencies -RUN npm ci --only-production +RUN npm ci --only-production --ignore-scripts # Rebuild the source code only when needed diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index 2bae23823f..cb462bbc4d 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -9,7 +9,7 @@ COPY package.json ./ COPY package-lock.json ./ # Install -RUN npm install +RUN npm install --ignore-scripts # Copy over next.js config COPY next.config.js ./next.config.js @@ -17,4 +17,4 @@ COPY next.config.js ./next.config.js # Copy all files COPY . . -CMD ["npm", "run", "dev"] \ No newline at end of file +CMD ["npm", "run", "dev"] diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod deleted file mode 100644 index d95c00883f..0000000000 --- a/frontend/Dockerfile.prod +++ /dev/null @@ -1,20 +0,0 @@ -# Base layer -FROM node:16-alpine - -# Set the working directory -WORKDIR /app - -# Copy over dependency files -COPY package.json ./ -COPY package-lock.json ./ - -# Install -RUN npm install - -# Copy over next.js config -COPY next.config.js ./next.config.js - -# Copy all files -COPY . . - -CMD ["npm", "run", "start:docker"] diff --git a/frontend/components/basic/Listbox.tsx b/frontend/components/basic/Listbox.tsx index aa8c41f85d..e726b7f56c 100644 --- a/frontend/components/basic/Listbox.tsx +++ b/frontend/components/basic/Listbox.tsx @@ -10,10 +10,10 @@ import { Listbox, Transition } from "@headlessui/react"; interface ListBoxProps { selected: string; - onChange: () => void; - data: string[]; - text: string; - buttonAction: () => void; + onChange: (arg: string) => void; + data: string[] | null; + text?: string; + buttonAction?: () => void; isFull?: boolean; } diff --git a/frontend/components/basic/buttons/Button.tsx b/frontend/components/basic/buttons/Button.tsx index 9f8d481b4f..562a82a36f 100644 --- a/frontend/components/basic/buttons/Button.tsx +++ b/frontend/components/basic/buttons/Button.tsx @@ -9,7 +9,7 @@ import { const classNames = require("classnames"); type ButtonProps = { - text: string; + text?: string; onButtonPressed: () => void; loading?: boolean; color?: string; diff --git a/frontend/components/integrations/CloudIntegration.tsx b/frontend/components/integrations/CloudIntegration.tsx index f64268dbac..75a8019a5e 100644 --- a/frontend/components/integrations/CloudIntegration.tsx +++ b/frontend/components/integrations/CloudIntegration.tsx @@ -1,30 +1,32 @@ import React from "react"; import Image from "next/image"; import { useRouter } from "next/router"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faX, } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + import deleteIntegrationAuth from "../../pages/api/integrations/DeleteIntegrationAuth"; interface CloudIntegrationOption { - isAvailable: Boolean; + isAvailable: boolean; name: string; type: string; clientId: string; docsLink: string; + slug: string; } interface IntegrationAuth { - id: string; + _id: string; integration: string; } interface Props { cloudIntegrationOption: CloudIntegrationOption; - setSelectedIntegrationOption: () => void; - integrationOptionPress: () => void; + setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void; + integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void; integrationAuths: IntegrationAuth[]; } @@ -45,9 +47,7 @@ const CloudIntegration = ({ onClick={() => { if (!cloudIntegrationOption.isAvailable) return; setSelectedIntegrationOption(cloudIntegrationOption); - integrationOptionPress({ - integrationOption: cloudIntegrationOption - }); + integrationOptionPress(cloudIntegrationOption); }} key={cloudIntegrationOption.name} > diff --git a/frontend/components/integrations/CloudIntegrationSection.tsx b/frontend/components/integrations/CloudIntegrationSection.tsx index 87ff74df14..58fb92cb2a 100644 --- a/frontend/components/integrations/CloudIntegrationSection.tsx +++ b/frontend/components/integrations/CloudIntegrationSection.tsx @@ -1,11 +1,14 @@ import React from "react"; + import CloudIntegration from "./CloudIntegration"; interface CloudIntegrationOption { + isAvailable: boolean; name: string; type: string; clientId: string; docsLink: string; + slug: string; } interface Props { diff --git a/frontend/components/integrations/FrameworkIntegrationSection.tsx b/frontend/components/integrations/FrameworkIntegrationSection.tsx index c83599dc1d..8535c595b0 100644 --- a/frontend/components/integrations/FrameworkIntegrationSection.tsx +++ b/frontend/components/integrations/FrameworkIntegrationSection.tsx @@ -1,14 +1,17 @@ import React from "react"; + import FrameworkIntegration from "./FrameworkIntegration"; interface Framework { name: string; image: string; link: string; + slug: string; + docsLink: string; } interface Props { - framework: Framework + frameworks: [Framework] } const FrameworkIntegrationSection = ({ frameworks }: Props) => { diff --git a/frontend/components/integrations/Integration.tsx b/frontend/components/integrations/Integration.tsx index a42fa5bcf8..053269be40 100644 --- a/frontend/components/integrations/Integration.tsx +++ b/frontend/components/integrations/Integration.tsx @@ -6,23 +6,33 @@ import { faX, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import Button from "~/components/basic/buttons/Button"; +import ListBox from "~/components/basic/Listbox"; + +import deleteIntegration from "../../pages/api/integrations/DeleteIntegration" +import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"; +import updateIntegration from "../../pages/api/integrations/updateIntegration" import { + contextNetlifyMapping, envMapping, + reverseContextNetlifyMapping, reverseEnvMapping, - reverseContextNetlifyMapping } from "../../public/data/frequentConstants"; -import updateIntegration from "../../pages/api/integrations/updateIntegration" -import deleteIntegration from "../../pages/api/integrations/DeleteIntegration" -import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"; -import Button from "~/components/basic/buttons/Button"; -import ListBox from "~/components/basic/Listbox"; interface Integration { + _id: string; app?: string; environment: string; integration: string; integrationAuth: string; - isActive: Boolean; + isActive: boolean; + context: string; +} + +interface IntegrationApp { + name: string; + siteId: string; } const Integration = ({ @@ -35,39 +45,44 @@ const Integration = ({ ); const [fileState, setFileState] = useState([]); const router = useRouter(); - const [apps, setApps] = useState([]); // integration app objects - const [integrationApp, setIntegrationApp] = useState(null); // integration app name - const [integrationTarget, setIntegrationTarget] = useState(null); // vercel-specific integration param - const [integrationContext, setIntegrationContext] = useState(null); // netlify-specific integration param + const [apps, setApps] = useState([]); // integration app objects + const [integrationApp, setIntegrationApp] = useState(""); // integration app name + const [integrationTarget, setIntegrationTarget] = useState(""); // vercel-specific integration param + const [integrationContext, setIntegrationContext] = useState(""); // netlify-specific integration param - useEffect(async () => { - interface App { - name: string; - siteId?: string; - } + useEffect(() => { - const tempApps = await getIntegrationApps({ - integrationAuthId: integration.integrationAuth, - }); - - setApps(tempApps); - setIntegrationApp( - integration.app ? integration.app : tempApps[0].name - ); - - switch (integration.integration) { - case "vercel": - setIntegrationTarget("Development"); - break; - case "netlify": - setIntegrationContext("All"); - break; - default: - break; + const loadIntegration = async () => { + interface App { + name: string; + siteId?: string; + } + + const tempApps: [IntegrationApp] = await getIntegrationApps({ + integrationAuthId: integration.integrationAuth, + }); + + setApps(tempApps); + setIntegrationApp( + integration.app ? integration.app : tempApps[0].name + ); + + switch (integration.integration) { + case "vercel": + setIntegrationTarget("Development"); + break; + case "netlify": + setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development"); + break; + default: + break; + } } + + loadIntegration(); }, []); - const renderIntegrationSpecificParams = (integration) => { + const renderIntegrationSpecificParams = (integration: Integration) => { try { switch (integration.integration) { case "vercel": @@ -77,11 +92,11 @@ const Integration = ({ ENVIRONMENT @@ -94,13 +109,12 @@ const Integration = ({ CONTEXT @@ -128,7 +142,9 @@ const Integration = ({ "Production", ] : null} selected={integrationEnvironment} - onChange={setIntegrationEnvironment} + onChange={(environment) => { + setIntegrationEnvironment(environment); + }} isFull={true} /> @@ -152,9 +168,11 @@ const Integration = ({ APP app.name)} + data={!integration.isActive ? apps.map((app) => app.name) : null} selected={integrationApp} - onChange={setIntegrationApp} + onChange={(app) => { + setIntegrationApp(app); + }} /> {renderIntegrationSpecificParams(integration)} @@ -172,7 +190,10 @@ const Integration = ({