diff --git a/.env.example b/.env.example index 23a0b8be0..e647ee38c 100644 --- a/.env.example +++ b/.env.example @@ -92,20 +92,24 @@ ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true # App Connections -# aws assume-role +# aws assume-role connection INF_APP_CONNECTION_AWS_ACCESS_KEY_ID= INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY= -# github oauth +# github oauth connection INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID= INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET= -#github app +#github app connection INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID= INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET= INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY= INF_APP_CONNECTION_GITHUB_APP_SLUG= INF_APP_CONNECTION_GITHUB_APP_ID= -#gcp app +#gcp app connection INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= + +# azure app connection +INF_APP_CONNECTION_AZURE_CLIENT_ID= +INF_APP_CONNECTION_AZURE_CLIENT_SECRET= \ No newline at end of file diff --git a/.github/workflows/deployment-pipeline.yml b/.github/workflows/deployment-pipeline.yml deleted file mode 100644 index f99412131..000000000 --- a/.github/workflows/deployment-pipeline.yml +++ /dev/null @@ -1,262 +0,0 @@ -name: Deployment pipeline -on: [workflow_dispatch] - -permissions: - id-token: write - contents: read - -concurrency: - group: "infisical-core-deployment" - cancel-in-progress: true - -jobs: - infisical-tests: - name: Integration tests - # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview - uses: ./.github/workflows/run-backend-tests.yml - - infisical-image: - name: Build - runs-on: ubuntu-latest - needs: [infisical-tests] - steps: - - name: ☁️ Checkout source - uses: actions/checkout@v3 - - name: 📦 Install dependencies to test all dependencies - run: npm ci --only-production - 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 push to docker hub - uses: depot/build-push-action@v1 - with: - project: 64mmf0n610 - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - push: true - context: . - file: Dockerfile.standalone-infisical - tags: | - infisical/staging_infisical:${{ steps.commit.outputs.short }} - infisical/staging_infisical:latest - platforms: linux/amd64,linux/arm64 - build-args: | - POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }} - - gamma-deployment: - name: Deploy to gamma - runs-on: ubuntu-latest - needs: [infisical-image] - environment: - name: Gamma - steps: - - uses: twingate/github-action@v1 - with: - # The Twingate Service Key used to connect Twingate to the proper service - # Learn more about [Twingate Services](https://docs.twingate.com/docs/services) - # - # Required - service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} - - name: Checkout code - uses: actions/checkout@v2 - - name: Setup Node.js environment - uses: actions/setup-node@v2 - with: - node-version: "20" - - name: Change directory to backend and install dependencies - env: - DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }} - run: | - cd backend - npm install - npm run migration:latest - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - audience: sts.amazonaws.com - aws-region: us-east-1 - role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition infisical-core-gamma-stage --query taskDefinition > task-definition.json - - name: Render Amazon ECS task definition - id: render-web-container - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: infisical-core - image: infisical/staging_infisical:${{ steps.commit.outputs.short }} - environment-variables: "LOG_LEVEL=info" - - name: Deploy to Amazon ECS service - uses: aws-actions/amazon-ecs-deploy-task-definition@v2 - with: - task-definition: ${{ steps.render-web-container.outputs.task-definition }} - service: infisical-core-gamma-stage - cluster: infisical-gamma-stage - wait-for-service-stability: true - - production-us: - name: US production deploy - runs-on: ubuntu-latest - needs: [gamma-deployment] - environment: - name: Production - steps: - - uses: twingate/github-action@v1 - with: - service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} - - name: Checkout code - uses: actions/checkout@v2 - - name: Setup Node.js environment - uses: actions/setup-node@v2 - with: - node-version: "20" - - name: Change directory to backend and install dependencies - env: - DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }} - AUDIT_LOGS_DB_CONNECTION_URI: ${{ secrets.AUDIT_LOGS_DB_CONNECTION_URI }} - run: | - cd backend - npm install - npm run migration:latest - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - audience: sts.amazonaws.com - aws-region: us-east-1 - role-to-assume: arn:aws:iam::381492033652:role/gha-make-prod-deployment - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json - - name: Render Amazon ECS task definition - id: render-web-container - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: infisical-core-platform - image: infisical/staging_infisical:${{ steps.commit.outputs.short }} - environment-variables: "LOG_LEVEL=info" - - name: Deploy to Amazon ECS service - uses: aws-actions/amazon-ecs-deploy-task-definition@v2 - with: - task-definition: ${{ steps.render-web-container.outputs.task-definition }} - service: infisical-core-platform - cluster: infisical-core-platform - wait-for-service-stability: true - - name: Post slack message - uses: slackapi/slack-github-action@v2.0.0 - with: - webhook: ${{ secrets.SLACK_DEPLOYMENT_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - text: "*Deployment Status Update*: ${{ job.status }}" - blocks: - - type: "section" - text: - type: "mrkdwn" - text: "*Deployment Status Update*: ${{ job.status }}" - - type: "section" - fields: - - type: "mrkdwn" - text: "*Application:*\nInfisical Core" - - type: "mrkdwn" - text: "*Instance Type:*\nShared Infisical Cloud" - - type: "section" - fields: - - type: "mrkdwn" - text: "*Region:*\nUS" - - type: "mrkdwn" - text: "*Git Tag:*\n" - - - production-eu: - name: EU production deploy - runs-on: ubuntu-latest - needs: [production-us] - environment: - name: production-eu - steps: - - uses: twingate/github-action@v1 - with: - service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - audience: sts.amazonaws.com - aws-region: eu-central-1 - role-to-assume: arn:aws:iam::345594589636:role/gha-make-prod-deployment - - name: Checkout code - uses: actions/checkout@v2 - - name: Setup Node.js environment - uses: actions/setup-node@v2 - with: - node-version: "20" - - name: Change directory to backend and install dependencies - env: - DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }} - run: | - cd backend - npm install - npm run migration:latest - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json - - name: Render Amazon ECS task definition - id: render-web-container - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: infisical-core-platform - image: infisical/staging_infisical:${{ steps.commit.outputs.short }} - environment-variables: "LOG_LEVEL=info" - - name: Deploy to Amazon ECS service - uses: aws-actions/amazon-ecs-deploy-task-definition@v2 - with: - task-definition: ${{ steps.render-web-container.outputs.task-definition }} - service: infisical-core-platform - cluster: infisical-core-platform - wait-for-service-stability: true - - name: Post slack message - uses: slackapi/slack-github-action@v2.0.0 - with: - webhook: ${{ secrets.SLACK_DEPLOYMENT_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - text: "*Deployment Status Update*: ${{ job.status }}" - blocks: - - type: "section" - text: - type: "mrkdwn" - text: "*Deployment Status Update*: ${{ job.status }}" - - type: "section" - fields: - - type: "mrkdwn" - text: "*Application:*\nInfisical Core" - - type: "mrkdwn" - text: "*Instance Type:*\nShared Infisical Cloud" - - type: "section" - fields: - - type: "mrkdwn" - text: "*Region:*\nEU" - - type: "mrkdwn" - text: "*Git Tag:*\n" - diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 58f2bffeb..9dfb38aeb 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -23,14 +23,14 @@ export default { name: "knex-env", transformMode: "ssr", async setup() { - const logger = await initLogger(); - const cfg = initEnvConfig(logger); + const logger = initLogger(); + const envConfig = initEnvConfig(logger); const db = initDbConnection({ - dbConnectionUri: cfg.DB_CONNECTION_URI, - dbRootCert: cfg.DB_ROOT_CERT + dbConnectionUri: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT }); - const redis = new Redis(cfg.REDIS_URL); + const redis = new Redis(envConfig.REDIS_URL); await redis.flushdb("SYNC"); try { @@ -42,6 +42,7 @@ export default { }, true ); + await db.migrate.latest({ directory: path.join(__dirname, "../src/db/migrations"), extension: "ts", @@ -52,14 +53,24 @@ export default { directory: path.join(__dirname, "../src/db/seeds"), extension: "ts" }); - const smtp = mockSmtpServer(); - const queue = queueServiceFactory(cfg.REDIS_URL, { dbConnectionUrl: cfg.DB_CONNECTION_URI }); - const keyStore = keyStoreFactory(cfg.REDIS_URL); - const hsmModule = initializeHsmModule(); + const smtp = mockSmtpServer(); + const queue = queueServiceFactory(envConfig.REDIS_URL, { dbConnectionUrl: envConfig.DB_CONNECTION_URI }); + const keyStore = keyStoreFactory(envConfig.REDIS_URL); + + const hsmModule = initializeHsmModule(envConfig); hsmModule.initialize(); - const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule(), redis }); + const server = await main({ + db, + smtp, + logger, + queue, + keyStore, + hsmModule: hsmModule.getModule(), + redis, + envConfig + }); // @ts-expect-error type globalThis.testServer = server; @@ -73,8 +84,8 @@ export default { organizationId: seedData1.organization.id, accessVersion: 1 }, - cfg.AUTH_SECRET, - { expiresIn: cfg.JWT_AUTH_LIFETIME } + envConfig.AUTH_SECRET, + { expiresIn: envConfig.JWT_AUTH_LIFETIME } ); } catch (error) { // eslint-disable-next-line @@ -109,3 +120,4 @@ export default { }; } }; + diff --git a/backend/package-lock.json b/backend/package-lock.json index e3c7bc6de..6373ef244 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,7 +21,7 @@ "@fastify/etag": "^5.1.0", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/multipart": "8.3.0", + "@fastify/multipart": "^8.3.1", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", "@fastify/request-context": "^5.1.0", @@ -48,8 +48,8 @@ "@peculiar/x509": "^1.12.1", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "1.1.0", - "@slack/oauth": "^3.0.1", - "@slack/web-api": "^7.3.4", + "@slack/oauth": "^3.0.2", + "@slack/web-api": "^7.8.0", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", @@ -81,7 +81,7 @@ "mongodb": "^6.8.1", "ms": "^2.1.3", "mysql2": "^3.9.8", - "nanoid": "^3.3.4", + "nanoid": "^3.3.8", "nodemailer": "^6.9.9", "odbc": "^2.4.9", "openid-client": "^5.6.5", @@ -5423,13 +5423,10 @@ } }, "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "license": "MIT", - "engines": { - "node": ">=14" - } + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", + "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", + "license": "MIT" }, "node_modules/@fastify/cookie": { "version": "9.3.1", @@ -5502,19 +5499,41 @@ } }, "node_modules/@fastify/multipart": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.3.0.tgz", - "integrity": "sha512-A8h80TTyqUzaMVH0Cr9Qcm6RxSkVqmhK/MVBYHYeRRSUbUYv08WecjWKSlG2aSnD4aGI841pVxAjC+G1GafUeQ==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.3.1.tgz", + "integrity": "sha512-pncbnG28S6MIskFSVRtzTKE9dK+GrKAJl0NbaQ/CG8ded80okWFsYKzSlP9haaLNQhNRDOoHqmGQNvgbiPVpWQ==", "license": "MIT", "dependencies": { - "@fastify/busboy": "^2.1.0", - "@fastify/deepmerge": "^1.0.0", - "@fastify/error": "^3.0.0", + "@fastify/busboy": "^3.0.0", + "@fastify/deepmerge": "^2.0.0", + "@fastify/error": "^4.0.0", "fastify-plugin": "^4.0.0", "secure-json-parse": "^2.4.0", "stream-wormhole": "^1.1.0" } }, + "node_modules/@fastify/multipart/node_modules/@fastify/deepmerge": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-2.0.1.tgz", + "integrity": "sha512-hx+wJQr9Ph1hY/dyzY0SxqjumMyqZDlIF6oe71dpRKDHUg7dFQfjG94qqwQ274XRjmUrwKiYadex8XplNHx3CA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/multipart/node_modules/@fastify/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", + "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==", + "license": "MIT" + }, "node_modules/@fastify/passport": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-2.4.0.tgz", @@ -9049,6 +9068,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz", "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==", + "license": "MIT", "dependencies": { "@types/node": ">=18.0.0" }, @@ -9058,12 +9078,13 @@ } }, "node_modules/@slack/oauth": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-3.0.1.tgz", - "integrity": "sha512-TuR9PI6bYKX6qHC7FQI4keMnhj45TNfSNQtTU3mtnHUX4XLM2dYLvRkUNADyiLTle2qu2rsOQtCIsZJw6H0sDA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-3.0.2.tgz", + "integrity": "sha512-MdPS8AP9n3u/hBeqRFu+waArJLD/q+wOSZ48ktMTwxQLc6HJyaWPf8soqAyS/b0D6IlvI5TxAdyRyyv3wQ5IVw==", + "license": "MIT", "dependencies": { "@slack/logger": "^4", - "@slack/web-api": "^7.3.4", + "@slack/web-api": "^7.8.0", "@types/jsonwebtoken": "^9", "@types/node": ">=18", "jsonwebtoken": "^9", @@ -9075,24 +9096,26 @@ } }, "node_modules/@slack/types": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.12.0.tgz", - "integrity": "sha512-yFewzUomYZ2BYaGJidPuIgjoYj5wqPDmi7DLSaGIkf+rCi4YZ2Z3DaiYIbz7qb/PL2NmamWjCvB7e9ArI5HkKg==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.14.0.tgz", + "integrity": "sha512-n0EGm7ENQRxlXbgKSrQZL69grzg1gHLAVd+GlRVQJ1NSORo0FrApR7wql/gaKdu2n4TO83Sq/AmeUOqD60aXUA==", + "license": "MIT", "engines": { "node": ">= 12.13.0", "npm": ">= 6.12.0" } }, "node_modules/@slack/web-api": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.3.4.tgz", - "integrity": "sha512-KwLK8dlz2lhr3NO7kbYQ7zgPTXPKrhq1JfQc0etJ0K8LSJhYYnf8GbVznvgDT/Uz1/pBXfFQnoXjrQIOKAdSuw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.8.0.tgz", + "integrity": "sha512-d4SdG+6UmGdzWw38a4sN3lF/nTEzsDxhzU13wm10ejOpPehtmRoqBKnPztQUfFiWbNvSb4czkWYJD4kt+5+Fuw==", + "license": "MIT", "dependencies": { "@slack/logger": "^4.0.0", "@slack/types": "^2.9.0", "@types/node": ">=18.0.0", "@types/retry": "0.12.0", - "axios": "^1.7.4", + "axios": "^1.7.8", "eventemitter3": "^5.0.1", "form-data": "^4.0.0", "is-electron": "2.2.2", @@ -9110,6 +9133,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -10526,7 +10550,8 @@ "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" }, "node_modules/@types/safe-regex": { "version": "1.1.6", @@ -11969,9 +11994,10 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -13926,7 +13952,8 @@ "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", @@ -15942,7 +15969,8 @@ "node_modules/is-electron": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", - "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" }, "node_modules/is-extglob": { "version": "2.1.1", @@ -18182,6 +18210,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", "engines": { "node": ">=4" } @@ -18228,6 +18257,7 @@ "version": "6.6.2", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" @@ -18242,12 +18272,14 @@ "node_modules/p-queue/node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -18271,6 +18303,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", "dependencies": { "p-finally": "^1.0.0" }, diff --git a/backend/package.json b/backend/package.json index 43409e619..6fa2f6554 100644 --- a/backend/package.json +++ b/backend/package.json @@ -45,24 +45,24 @@ "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts", "generate:component": "tsx ./scripts/create-backend-file.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas", - "auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest", - "auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up", - "auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down", - "auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list", - "auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status", - "auditlog-migration:unlock": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:unlock", - "auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback", + "auditlog-migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:latest", + "auditlog-migration:up": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:up", + "auditlog-migration:down": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:down", + "auditlog-migration:list": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:list", + "auditlog-migration:status": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:status", + "auditlog-migration:unlock": "knex --knexfile ./dist/db/auditlog-knexfile.mjs migrate:unlock", + "auditlog-migration:rollback": "knex --knexfile ./dist/db/auditlog-knexfile.mjs migrate:rollback", "migration:new": "tsx ./scripts/create-migration.ts", - "migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up", - "migration:down": "npm run auditlog-migration:down && knex --knexfile ./src/db/knexfile.ts --client pg migrate:down", - "migration:list": "npm run auditlog-migration:list && knex --knexfile ./src/db/knexfile.ts --client pg migrate:list", - "migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", - "migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status", - "migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback", - "migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./src/db/knexfile.ts migrate:unlock", + "migration:up": "npm run auditlog-migration:up && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:up", + "migration:down": "npm run auditlog-migration:down && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:down", + "migration:list": "npm run auditlog-migration:list && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:list", + "migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && npm run auditlog-migration:latest && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:latest", + "migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status", + "migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback", + "migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock", "migrate:org": "tsx ./scripts/migrate-organization.ts", "seed:new": "tsx ./scripts/create-seed-file.ts", - "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", + "seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run", "db:reset": "npm run migration:rollback -- --all && npm run migration:latest" }, "keywords": [], @@ -129,7 +129,7 @@ "@fastify/etag": "^5.1.0", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/multipart": "8.3.0", + "@fastify/multipart": "8.3.1", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", "@fastify/request-context": "^5.1.0", @@ -156,8 +156,8 @@ "@peculiar/x509": "^1.12.1", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "1.1.0", - "@slack/oauth": "^3.0.1", - "@slack/web-api": "^7.3.4", + "@slack/oauth": "^3.0.2", + "@slack/web-api": "^7.8.0", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", @@ -189,7 +189,7 @@ "mongodb": "^6.8.1", "ms": "^2.1.3", "mysql2": "^3.9.8", - "nanoid": "^3.3.4", + "nanoid": "^3.3.8", "nodemailer": "^6.9.9", "odbc": "^2.4.9", "openid-client": "^5.6.5", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index b05d3184a..1cb11545c 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -96,6 +96,12 @@ import { TUserEngagementServiceFactory } from "@app/services/user-engagement/use import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; +declare module "@fastify/request-context" { + interface RequestContextData { + reqId: string; + } +} + declare module "fastify" { interface Session { callbackPort: string; diff --git a/backend/src/auto-start-migrations.ts b/backend/src/auto-start-migrations.ts new file mode 100644 index 000000000..88f6dea69 --- /dev/null +++ b/backend/src/auto-start-migrations.ts @@ -0,0 +1,105 @@ +import path from "node:path"; + +import dotenv from "dotenv"; +import { Knex } from "knex"; +import { Logger } from "pino"; + +import { PgSqlLock } from "./keystore/keystore"; + +dotenv.config(); + +type TArgs = { + auditLogDb?: Knex; + applicationDb: Knex; + logger: Logger; +}; + +const isProduction = process.env.NODE_ENV === "production"; +const migrationConfig = { + directory: path.join(__dirname, "./db/migrations"), + loadExtensions: [".mjs", ".ts"], + tableName: "infisical_migrations" +}; + +const migrationStatusCheckErrorHandler = (err: Error) => { + // happens for first time in which the migration table itself is not created yet + // error: select * from "infisical_migrations" - relation "infisical_migrations" does not exist + if (err?.message?.includes("does not exist")) { + return true; + } + throw err; +}; + +export const runMigrations = async ({ applicationDb, auditLogDb, logger }: TArgs) => { + try { + // akhilmhdh(Feb 10 2025): 2 years from now remove this + if (isProduction) { + const migrationTable = migrationConfig.tableName; + const hasMigrationTable = await applicationDb.schema.hasTable(migrationTable); + if (hasMigrationTable) { + const firstFile = (await applicationDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await applicationDb(migrationTable).update({ + name: applicationDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + if (auditLogDb) { + const hasMigrationTableInAuditLog = await auditLogDb.schema.hasTable(migrationTable); + if (hasMigrationTableInAuditLog) { + const firstFile = (await auditLogDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await auditLogDb(migrationTable).update({ + name: auditLogDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + } + } + + const shouldRunMigration = Boolean( + await applicationDb.migrate.status(migrationConfig).catch(migrationStatusCheckErrorHandler) + ); // db.length - code.length + if (!shouldRunMigration) { + logger.info("No migrations pending: Skipping migration process."); + return; + } + + if (auditLogDb) { + await auditLogDb.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); + logger.info("Running audit log migrations."); + + const didPreviousInstanceRunMigration = !(await auditLogDb.migrate + .status(migrationConfig) + .catch(migrationStatusCheckErrorHandler)); + if (didPreviousInstanceRunMigration) { + logger.info("No audit log migrations pending: Applied by previous instance. Skipping migration process."); + return; + } + + await auditLogDb.migrate.latest(migrationConfig); + logger.info("Finished audit log migrations."); + }); + } + + await applicationDb.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); + logger.info("Running application migrations."); + + const didPreviousInstanceRunMigration = !(await applicationDb.migrate + .status(migrationConfig) + .catch(migrationStatusCheckErrorHandler)); + if (didPreviousInstanceRunMigration) { + logger.info("No application migrations pending: Applied by previous instance. Skipping migration process."); + return; + } + + await applicationDb.migrate.latest(migrationConfig); + logger.info("Finished application migrations."); + }); + } catch (err) { + logger.error(err, "Boot up migration failed"); + process.exit(1); + } +}; diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index d4a2a5b2c..5a8dd3d05 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -49,6 +49,9 @@ export const initDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); @@ -64,6 +67,9 @@ export const initDbConnection = ({ ca: Buffer.from(replicaDbCertificate, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); }); @@ -98,6 +104,9 @@ export const initAuditLogDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); diff --git a/backend/src/db/knexfile.ts b/backend/src/db/knexfile.ts index 8af2b59ab..8cf80b744 100644 --- a/backend/src/db/knexfile.ts +++ b/backend/src/db/knexfile.ts @@ -38,7 +38,8 @@ export default { directory: "./seeds" }, migrations: { - tableName: "infisical_migrations" + tableName: "infisical_migrations", + loadExtensions: [".mjs"] } }, production: { @@ -62,7 +63,8 @@ export default { max: 10 }, migrations: { - tableName: "infisical_migrations" + tableName: "infisical_migrations", + loadExtensions: [".mjs"] } } } as Knex.Config; diff --git a/backend/src/db/migrations/20250205045509_increase-gcp-auth-limit.ts b/backend/src/db/migrations/20250205045509_increase-gcp-auth-limit.ts index 02f3904d5..6d5e7cc4a 100644 --- a/backend/src/db/migrations/20250205045509_increase-gcp-auth-limit.ts +++ b/backend/src/db/migrations/20250205045509_increase-gcp-auth-limit.ts @@ -1,4 +1,5 @@ import { Knex } from "knex"; + import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { diff --git a/backend/src/db/migrations/20250205220952_kms-keys-drop-slug-col.ts b/backend/src/db/migrations/20250205220952_kms-keys-drop-slug-col.ts new file mode 100644 index 000000000..525e459b3 --- /dev/null +++ b/backend/src/db/migrations/20250205220952_kms-keys-drop-slug-col.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.KmsKey)) { + const hasSlugCol = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + + if (hasSlugCol) { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.dropColumn("slug"); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.KmsKey)) { + const hasSlugCol = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + + if (!hasSlugCol) { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.string("slug", 32); + }); + } + } +} diff --git a/backend/src/db/migrations/20250207002643_secret-syncs-increase-message-length.ts b/backend/src/db/migrations/20250207002643_secret-syncs-increase-message-length.ts new file mode 100644 index 000000000..d550810aa --- /dev/null +++ b/backend/src/db/migrations/20250207002643_secret-syncs-increase-message-length.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSync)) { + const hasLastSyncMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastSyncMessage"); + const hasLastImportMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastImportMessage"); + const hasLastRemoveMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastRemoveMessage"); + + await knex.schema.alterTable(TableName.SecretSync, (t) => { + if (hasLastSyncMessage) t.string("lastSyncMessage", 1024).alter(); + if (hasLastImportMessage) t.string("lastImportMessage", 1024).alter(); + if (hasLastRemoveMessage) t.string("lastRemoveMessage", 1024).alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSync)) { + const hasLastSyncMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastSyncMessage"); + const hasLastImportMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastImportMessage"); + const hasLastRemoveMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastRemoveMessage"); + + await knex.schema.alterTable(TableName.SecretSync, (t) => { + if (hasLastSyncMessage) t.string("lastSyncMessage").alter(); + if (hasLastImportMessage) t.string("lastImportMessage").alter(); + if (hasLastRemoveMessage) t.string("lastRemoveMessage").alter(); + }); + } +} diff --git a/backend/src/db/migrations/20250210101840_webhook-to-kms.ts b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts new file mode 100644 index 000000000..a2d856388 --- /dev/null +++ b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts @@ -0,0 +1,130 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedPassKey"); + const hasEncryptedUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); + const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url"); + + const hasWebhookTable = await knex.schema.hasTable(TableName.Webhook); + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (!hasEncryptedKey) t.binary("encryptedPassKey"); + if (!hasEncryptedUrl) t.binary("encryptedUrl"); + if (hasUrl) t.string("url").nullable().alter(); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + const webhooks = await knex(TableName.Webhook) + .where({}) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`) + .select( + "url", + "encryptedSecretKey", + "iv", + "tag", + "keyEncoding", + "urlCipherText", + "urlIV", + "urlTag", + knex.ref("id").withSchema(TableName.Webhook), + "envId" + ) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedWebhooks = await Promise.all( + webhooks.map(async (el) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(el.projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId: el.projectId + }, + knex + ); + projectEncryptionRingBuffer.push(el.projectId, projectKmsService); + } + + let encryptedSecretKey = null; + if (el.encryptedSecretKey && el.iv && el.tag && el.keyEncoding) { + const decyptedSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + iv: el.iv, + tag: el.tag, + ciphertext: el.encryptedSecretKey + }); + encryptedSecretKey = projectKmsService.encryptor({ + plainText: Buffer.from(decyptedSecretKey, "utf8") + }).cipherTextBlob; + } + + const decryptedUrl = + el.urlIV && el.urlTag && el.urlCipherText && el.keyEncoding + ? infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + iv: el.urlIV, + tag: el.urlTag, + ciphertext: el.urlCipherText + }) + : null; + + const encryptedUrl = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedUrl || el.url || "") + }).cipherTextBlob; + return { id: el.id, encryptedUrl, encryptedSecretKey, envId: el.envId }; + }) + ); + + for (let i = 0; i < updatedWebhooks.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.Webhook) + .insert( + updatedWebhooks.slice(i, i + BATCH_SIZE).map((el) => ({ + id: el.id, + envId: el.envId, + url: "", + encryptedUrl: el.encryptedUrl, + encryptedPassKey: el.encryptedSecretKey + })) + ) + .onConflict("id") + .merge(); + } + + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (!hasEncryptedUrl) t.binary("encryptedUrl").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedPassKey"); + const hasEncryptedUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); + + const hasWebhookTable = await knex.schema.hasTable(TableName.Webhook); + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (hasEncryptedKey) t.dropColumn("encryptedPassKey"); + if (hasEncryptedUrl) t.dropColumn("encryptedUrl"); + }); + } +} diff --git a/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts new file mode 100644 index 000000000..dde1e7188 --- /dev/null +++ b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts @@ -0,0 +1,111 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedInputColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedInput"); + const hasInputCiphertextColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext"); + const hasInputIVColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV"); + const hasInputTagColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag"); + + const hasDynamicSecretTable = await knex.schema.hasTable(TableName.DynamicSecret); + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (!hasEncryptedInputColumn) t.binary("encryptedInput"); + if (hasInputCiphertextColumn) t.text("inputCiphertext").nullable().alter(); + if (hasInputIVColumn) t.string("inputIV").nullable().alter(); + if (hasInputTagColumn) t.string("inputTag").nullable().alter(); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + + const dynamicSecretRootCredentials = await knex(TableName.DynamicSecret) + .join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .select(selectAllTableCols(TableName.DynamicSecret)) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedDynamicSecrets = await Promise.all( + dynamicSecretRootCredentials.map(async ({ projectId, ...el }) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId + }, + knex + ); + projectEncryptionRingBuffer.push(projectId, projectKmsService); + } + + const decryptedInputData = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.inputIV && el.inputTag && el.inputCiphertext && el.keyEncoding + ? infisicalSymmetricDecrypt({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + keyEncoding: el.keyEncoding as SecretKeyEncoding, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.inputIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.inputTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.inputCiphertext + }) + : ""; + + const encryptedInput = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedInputData) + }).cipherTextBlob; + + return { ...el, encryptedInput }; + }) + ); + + for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.DynamicSecret) + .insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (!hasEncryptedInputColumn) t.binary("encryptedInput").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedInputColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedInput"); + + const hasDynamicSecretTable = await knex.schema.hasTable(TableName.DynamicSecret); + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (hasEncryptedInputColumn) t.dropColumn("encryptedInput"); + }); + } +} diff --git a/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts new file mode 100644 index 000000000..e11ef926e --- /dev/null +++ b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts @@ -0,0 +1,103 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedRotationData = await knex.schema.hasColumn(TableName.SecretRotation, "encryptedRotationData"); + + const hasRotationTable = await knex.schema.hasTable(TableName.SecretRotation); + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (!hasEncryptedRotationData) t.binary("encryptedRotationData"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + + const secretRotations = await knex(TableName.SecretRotation) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretRotation}.envId`) + .select(selectAllTableCols(TableName.SecretRotation)) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedRotationData = await Promise.all( + secretRotations.map(async ({ projectId, ...el }) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId + }, + knex + ); + projectEncryptionRingBuffer.push(projectId, projectKmsService); + } + + const decryptedRotationData = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedDataTag && el.encryptedDataIV && el.encryptedData && el.keyEncoding + ? infisicalSymmetricDecrypt({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + keyEncoding: el.keyEncoding as SecretKeyEncoding, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.encryptedDataIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.encryptedDataTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedData + }) + : ""; + + const encryptedRotationData = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedRotationData) + }).cipherTextBlob; + return { ...el, encryptedRotationData }; + }) + ); + + for (let i = 0; i < updatedRotationData.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.SecretRotation) + .insert(updatedRotationData.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (!hasEncryptedRotationData) t.binary("encryptedRotationData").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedRotationData = await knex.schema.hasColumn(TableName.SecretRotation, "encryptedRotationData"); + + const hasRotationTable = await knex.schema.hasTable(TableName.SecretRotation); + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (hasEncryptedRotationData) t.dropColumn("encryptedRotationData"); + }); + } +} diff --git a/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts new file mode 100644 index 000000000..934dce5e8 --- /dev/null +++ b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts @@ -0,0 +1,200 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptIdentityK8sAuth = async (knex: Knex) => { + const hasEncryptedKubernetesTokenReviewerJwt = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesTokenReviewerJwt" + ); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesCaCertificate" + ); + const hasidentityKubernetesAuthTable = await knex.schema.hasTable(TableName.IdentityKubernetesAuth); + + const hasEncryptedCaCertColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "encryptedCaCert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "caCertTag"); + const hasEncryptedTokenReviewerJwtColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedTokenReviewerJwt" + ); + const hasTokenReviewerJwtIVColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "tokenReviewerJwtIV" + ); + const hasTokenReviewerJwtTagColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "tokenReviewerJwtTag" + ); + + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (hasEncryptedCaCertColumn) t.text("encryptedCaCert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + if (hasEncryptedTokenReviewerJwtColumn) t.text("encryptedTokenReviewerJwt").nullable().alter(); + if (hasTokenReviewerJwtIVColumn) t.string("tokenReviewerJwtIV").nullable().alter(); + if (hasTokenReviewerJwtTagColumn) t.string("tokenReviewerJwtTag").nullable().alter(); + + if (!hasEncryptedKubernetesTokenReviewerJwt) t.binary("encryptedKubernetesTokenReviewerJwt"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedKubernetesCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + const identityKubernetesConfigs = await knex(TableName.IdentityKubernetesAuth) + .join( + TableName.IdentityOrgMembership, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.IdentityOrgMembership}.orgId`) + .select(selectAllTableCols(TableName.IdentityKubernetesAuth)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot), + knex.ref("orgId").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedIdentityKubernetesConfigs = []; + + for await (const { + encryptedSymmetricKey, + symmetricKeyKeyEncoding, + symmetricKeyTag, + symmetricKeyIV, + orgId, + ...el + } of identityKubernetesConfigs) { + let orgKmsService = orgEncryptionRingBuffer.getItem(orgId); + + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId + }, + knex + ); + orgEncryptionRingBuffer.push(orgId, orgKmsService); + } + + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedTokenReviewerJwt = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedTokenReviewerJwt && el.tokenReviewerJwtIV && el.tokenReviewerJwtTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.tokenReviewerJwtIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.tokenReviewerJwtTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedTokenReviewerJwt + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCaCert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCaCert + }) + : ""; + + const encryptedKubernetesTokenReviewerJwt = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedTokenReviewerJwt) + }).cipherTextBlob; + const encryptedKubernetesCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + + updatedIdentityKubernetesConfigs.push({ + ...el, + accessTokenTrustedIps: JSON.stringify(el.accessTokenTrustedIps), + encryptedKubernetesCaCertificate, + encryptedKubernetesTokenReviewerJwt + }); + } + + for (let i = 0; i < updatedIdentityKubernetesConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityKubernetesAuth) + .insert(updatedIdentityKubernetesConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (!hasEncryptedKubernetesTokenReviewerJwt) + t.binary("encryptedKubernetesTokenReviewerJwt").notNullable().alter(); + }); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptIdentityK8sAuth(knex); +} + +const dropIdentityK8sColumns = async (knex: Knex) => { + const hasEncryptedKubernetesTokenReviewerJwt = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesTokenReviewerJwt" + ); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesCaCertificate" + ); + const hasidentityKubernetesAuthTable = await knex.schema.hasTable(TableName.IdentityKubernetesAuth); + + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (hasEncryptedKubernetesTokenReviewerJwt) t.dropColumn("encryptedKubernetesTokenReviewerJwt"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedKubernetesCaCertificate"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropIdentityK8sColumns(knex); +} diff --git a/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts new file mode 100644 index 000000000..011585bda --- /dev/null +++ b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts @@ -0,0 +1,141 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptIdentityOidcAuth = async (knex: Knex) => { + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityOidcAuth, + "encryptedCaCertificate" + ); + const hasidentityOidcAuthTable = await knex.schema.hasTable(TableName.IdentityOidcAuth); + + const hasEncryptedCaCertColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "encryptedCaCert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "caCertTag"); + + if (hasidentityOidcAuthTable) { + await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => { + if (hasEncryptedCaCertColumn) t.text("encryptedCaCert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + + if (!hasEncryptedCertificateColumn) t.binary("encryptedCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const identityOidcConfig = await knex(TableName.IdentityOidcAuth) + .join( + TableName.IdentityOrgMembership, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityOidcAuth}.identityId` + ) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.IdentityOrgMembership}.orgId`) + .select(selectAllTableCols(TableName.IdentityOidcAuth)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot), + knex.ref("orgId").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedIdentityOidcConfigs = await Promise.all( + identityOidcConfig.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, orgId, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId + }, + knex + ); + orgEncryptionRingBuffer.push(orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCaCert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCaCert + }) + : ""; + + const encryptedCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + + return { + ...el, + accessTokenTrustedIps: JSON.stringify(el.accessTokenTrustedIps), + encryptedCaCertificate + }; + } + ) + ); + + for (let i = 0; i < updatedIdentityOidcConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityOidcAuth) + .insert(updatedIdentityOidcConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptIdentityOidcAuth(knex); +} + +const dropIdentityOidcColumns = async (knex: Knex) => { + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityOidcAuth, + "encryptedCaCertificate" + ); + const hasidentityOidcTable = await knex.schema.hasTable(TableName.IdentityOidcAuth); + + if (hasidentityOidcTable) { + await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => { + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedCaCertificate"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropIdentityOidcColumns(knex); +} diff --git a/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts new file mode 100644 index 000000000..f5107b301 --- /dev/null +++ b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts @@ -0,0 +1,493 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptSamlConfig = async (knex: Knex) => { + const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint"); + const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate"); + const hasSamlConfigTable = await knex.schema.hasTable(TableName.SamlConfig); + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (!hasEncryptedEntrypointColumn) t.binary("encryptedSamlEntryPoint"); + if (!hasEncryptedIssuerColumn) t.binary("encryptedSamlIssuer"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedSamlCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const samlConfigs = await knex(TableName.SamlConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.SamlConfig}.orgId`) + .select(selectAllTableCols(TableName.SamlConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedSamlConfigs = await Promise.all( + samlConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedEntryPoint = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedEntryPoint && el.entryPointIV && el.entryPointTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.entryPointIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.entryPointTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedEntryPoint + }) + : ""; + + const decryptedIssuer = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedIssuer && el.issuerIV && el.issuerTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.issuerIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.issuerTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedIssuer + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCert && el.certIV && el.certTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.certIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.certTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCert + }) + : ""; + + const encryptedSamlIssuer = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedIssuer) + }).cipherTextBlob; + const encryptedSamlCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + const encryptedSamlEntryPoint = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedEntryPoint) + }).cipherTextBlob; + return { ...el, encryptedSamlCertificate, encryptedSamlEntryPoint, encryptedSamlIssuer }; + } + ) + ); + + for (let i = 0; i < updatedSamlConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.SamlConfig) + .insert(updatedSamlConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (!hasEncryptedEntrypointColumn) t.binary("encryptedSamlEntryPoint").notNullable().alter(); + if (!hasEncryptedIssuerColumn) t.binary("encryptedSamlIssuer").notNullable().alter(); + if (!hasEncryptedCertificateColumn) t.binary("encryptedSamlCertificate").notNullable().alter(); + }); + } +}; + +const reencryptLdapConfig = async (knex: Knex) => { + const hasEncryptedLdapBindDNColum = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN"); + const hasEncryptedLdapBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate"); + const hasLdapConfigTable = await knex.schema.hasTable(TableName.LdapConfig); + + const hasEncryptedCACertColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedCACert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "caCertTag"); + const hasEncryptedBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedBindPass"); + const hasBindPassIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindPassIV"); + const hasBindPassTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindPassTag"); + const hasEncryptedBindDNColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedBindDN"); + const hasBindDNIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindDNIV"); + const hasBindDNTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindDNTag"); + + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (hasEncryptedCACertColumn) t.text("encryptedCACert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + if (hasEncryptedBindPassColumn) t.string("encryptedBindPass").nullable().alter(); + if (hasBindPassIVColumn) t.string("bindPassIV").nullable().alter(); + if (hasBindPassTagColumn) t.string("bindPassTag").nullable().alter(); + if (hasEncryptedBindDNColumn) t.string("encryptedBindDN").nullable().alter(); + if (hasBindDNIVColumn) t.string("bindDNIV").nullable().alter(); + if (hasBindDNTagColumn) t.string("bindDNTag").nullable().alter(); + + if (!hasEncryptedLdapBindDNColum) t.binary("encryptedLdapBindDN"); + if (!hasEncryptedLdapBindPassColumn) t.binary("encryptedLdapBindPass"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedLdapCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const ldapConfigs = await knex(TableName.LdapConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.LdapConfig}.orgId`) + .select(selectAllTableCols(TableName.LdapConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedLdapConfigs = await Promise.all( + ldapConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedBindDN = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedBindDN && el.bindDNIV && el.bindDNTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.bindDNIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.bindDNTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedBindDN + }) + : ""; + + const decryptedBindPass = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedBindPass && el.bindPassIV && el.bindPassTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.bindPassIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.bindPassTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedBindPass + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCACert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCACert + }) + : ""; + + const encryptedLdapBindDN = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedBindDN) + }).cipherTextBlob; + const encryptedLdapBindPass = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedBindPass) + }).cipherTextBlob; + const encryptedLdapCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + return { ...el, encryptedLdapBindPass, encryptedLdapBindDN, encryptedLdapCaCertificate }; + } + ) + ); + + for (let i = 0; i < updatedLdapConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.LdapConfig) + .insert(updatedLdapConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (!hasEncryptedLdapBindPassColumn) t.binary("encryptedLdapBindPass").notNullable().alter(); + if (!hasEncryptedLdapBindDNColum) t.binary("encryptedLdapBindDN").notNullable().alter(); + }); + } +}; + +const reencryptOidcConfig = async (knex: Knex) => { + const hasEncryptedOidcClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId"); + const hasEncryptedOidcClientSecretColumn = await knex.schema.hasColumn( + TableName.OidcConfig, + "encryptedOidcClientSecret" + ); + + const hasEncryptedClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedClientId"); + const hasClientIdIVColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientIdIV"); + const hasClientIdTagColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientIdTag"); + const hasEncryptedClientSecretColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedClientSecret"); + const hasClientSecretIVColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientSecretIV"); + const hasClientSecretTagColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientSecretTag"); + + const hasOidcConfigTable = await knex.schema.hasTable(TableName.OidcConfig); + + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (hasEncryptedClientIdColumn) t.text("encryptedClientId").nullable().alter(); + if (hasClientIdIVColumn) t.string("clientIdIV").nullable().alter(); + if (hasClientIdTagColumn) t.string("clientIdTag").nullable().alter(); + if (hasEncryptedClientSecretColumn) t.text("encryptedClientSecret").nullable().alter(); + if (hasClientSecretIVColumn) t.string("clientSecretIV").nullable().alter(); + if (hasClientSecretTagColumn) t.string("clientSecretTag").nullable().alter(); + + if (!hasEncryptedOidcClientIdColumn) t.binary("encryptedOidcClientId"); + if (!hasEncryptedOidcClientSecretColumn) t.binary("encryptedOidcClientSecret"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const oidcConfigs = await knex(TableName.OidcConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.OidcConfig}.orgId`) + .select(selectAllTableCols(TableName.OidcConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedOidcConfigs = await Promise.all( + oidcConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedClientId = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedClientId && el.clientIdIV && el.clientIdTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.clientIdIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.clientIdTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedClientId + }) + : ""; + + const decryptedClientSecret = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedClientSecret && el.clientSecretIV && el.clientSecretTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.clientSecretIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.clientSecretTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedClientSecret + }) + : ""; + + const encryptedOidcClientId = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedClientId) + }).cipherTextBlob; + const encryptedOidcClientSecret = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedClientSecret) + }).cipherTextBlob; + return { ...el, encryptedOidcClientId, encryptedOidcClientSecret }; + } + ) + ); + + for (let i = 0; i < updatedOidcConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.OidcConfig) + .insert(updatedOidcConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (!hasEncryptedOidcClientIdColumn) t.binary("encryptedOidcClientId").notNullable().alter(); + if (!hasEncryptedOidcClientSecretColumn) t.binary("encryptedOidcClientSecret").notNullable().alter(); + }); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptSamlConfig(knex); + await reencryptLdapConfig(knex); + await reencryptOidcConfig(knex); +} + +const dropSamlConfigColumns = async (knex: Knex) => { + const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint"); + const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate"); + const hasSamlConfigTable = await knex.schema.hasTable(TableName.SamlConfig); + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (hasEncryptedEntrypointColumn) t.dropColumn("encryptedSamlEntryPoint"); + if (hasEncryptedIssuerColumn) t.dropColumn("encryptedSamlIssuer"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedSamlCertificate"); + }); + } +}; + +const dropLdapConfigColumns = async (knex: Knex) => { + const hasEncryptedBindDN = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN"); + const hasEncryptedBindPass = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate"); + const hasLdapConfigTable = await knex.schema.hasTable(TableName.LdapConfig); + + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (hasEncryptedBindDN) t.dropColumn("encryptedLdapBindDN"); + if (hasEncryptedBindPass) t.dropColumn("encryptedLdapBindPass"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedLdapCaCertificate"); + }); + } +}; + +const dropOidcConfigColumns = async (knex: Knex) => { + const hasEncryptedClientId = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId"); + const hasEncryptedClientSecret = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientSecret"); + const hasOidcConfigTable = await knex.schema.hasTable(TableName.OidcConfig); + + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (hasEncryptedClientId) t.dropColumn("encryptedOidcClientId"); + if (hasEncryptedClientSecret) t.dropColumn("encryptedOidcClientSecret"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropSamlConfigColumns(knex); + await dropLdapConfigColumns(knex); + await dropOidcConfigColumns(knex); +} diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts new file mode 100644 index 000000000..8744308ab --- /dev/null +++ b/backend/src/db/migrations/utils/env-config.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +import { zpStr } from "@app/lib/zod"; + +const envSchema = z + .object({ + DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( + `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` + ), + DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()), + DB_HOST: zpStr(z.string().describe("Postgres database host").optional()), + DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"), + DB_USER: zpStr(z.string().describe("Postgres database username").optional()), + DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()), + DB_NAME: zpStr(z.string().describe("Postgres database name").optional()), + // TODO(akhilmhdh): will be changed to one + ENCRYPTION_KEY: zpStr(z.string().optional()), + ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()), + // HSM + HSM_LIB_PATH: zpStr(z.string().optional()), + HSM_PIN: zpStr(z.string().optional()), + HSM_KEY_LABEL: zpStr(z.string().optional()), + HSM_SLOT: z.coerce.number().optional().default(0) + }) + // To ensure that basic encryption is always possible. + .refine( + (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), + "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." + ) + .transform((data) => ({ + ...data, + isHsmConfigured: + Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined + })); + +export type TMigrationEnvConfig = z.infer; + +export const getMigrationEnvConfig = () => { + const parsedEnv = envSchema.safeParse(process.env); + if (!parsedEnv.success) { + // eslint-disable-next-line no-console + console.error("Invalid environment variables. Check the error below"); + // eslint-disable-next-line no-console + console.error( + "Infisical now automatically runs database migrations during boot up, so you no longer need to run them separately." + ); + // eslint-disable-next-line no-console + console.error(parsedEnv.error.issues); + process.exit(-1); + } + + return Object.freeze(parsedEnv.data); +}; diff --git a/backend/src/db/migrations/utils/kms.ts b/backend/src/db/migrations/utils/kms.ts deleted file mode 100644 index 9ed090978..000000000 --- a/backend/src/db/migrations/utils/kms.ts +++ /dev/null @@ -1,105 +0,0 @@ -import slugify from "@sindresorhus/slugify"; -import { Knex } from "knex"; - -import { TableName } from "@app/db/schemas"; -import { randomSecureBytes } from "@app/lib/crypto"; -import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; - -const getInstanceRootKey = async (knex: Knex) => { - const encryptionKey = process.env.ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY; - // if root key its base64 encoded - const isBase64 = !process.env.ENCRYPTION_KEY; - if (!encryptionKey) throw new Error("ENCRYPTION_KEY variable needed for migration"); - const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); - - const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; - const kmsRootConfig = await knex(TableName.KmsServerRootConfig).where({ id: KMS_ROOT_CONFIG_UUID }).first(); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - if (kmsRootConfig) { - const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); - // set the flag so that other instancen nodes can start - return decryptedRootKey; - } - - const newRootKey = randomSecureBytes(32); - const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); - await knex(TableName.KmsServerRootConfig).insert({ - encryptedRootKey, - // eslint-disable-next-line - // @ts-ignore id is kept as fixed for idempotence and to avoid race condition - id: KMS_ROOT_CONFIG_UUID - }); - return encryptedRootKey; -}; - -export const getSecretManagerDataKey = async (knex: Knex, projectId: string) => { - const KMS_VERSION = "v01"; - const KMS_VERSION_BLOB_LENGTH = 3; - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const project = await knex(TableName.Project).where({ id: projectId }).first(); - if (!project) throw new Error("Missing project id"); - - const ROOT_ENCRYPTION_KEY = await getInstanceRootKey(knex); - - let secretManagerKmsKey; - const projectSecretManagerKmsId = project?.kmsSecretManagerKeyId; - if (projectSecretManagerKmsId) { - const kmsDoc = await knex(TableName.KmsKey) - .leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) - .where({ [`${TableName.KmsKey}.id` as "id"]: projectSecretManagerKmsId }) - .first(); - if (!kmsDoc) throw new Error("missing kms"); - secretManagerKmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY); - } else { - const [kmsDoc] = await knex(TableName.KmsKey) - .insert({ - name: slugify(alphaNumericNanoId(8).toLowerCase()), - orgId: project.orgId, - isReserved: false - }) - .returning("*"); - - secretManagerKmsKey = randomSecureBytes(32); - const encryptedKeyMaterial = cipher.encrypt(secretManagerKmsKey, ROOT_ENCRYPTION_KEY); - await knex(TableName.InternalKms).insert({ - version: 1, - encryptedKey: encryptedKeyMaterial, - encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, - kmsKeyId: kmsDoc.id - }); - } - - const encryptedSecretManagerDataKey = project?.kmsSecretManagerEncryptedDataKey; - let dataKey: Buffer; - if (!encryptedSecretManagerDataKey) { - dataKey = randomSecureBytes(); - // the below versioning we do it automatically in kms service - const unversionedDataKey = cipher.encrypt(dataKey, secretManagerKmsKey); - const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 - await knex(TableName.Project) - .where({ id: projectId }) - .update({ - kmsSecretManagerEncryptedDataKey: Buffer.concat([unversionedDataKey, versionBlob]) - }); - } else { - const cipherTextBlob = encryptedSecretManagerDataKey.subarray(0, -KMS_VERSION_BLOB_LENGTH); - dataKey = cipher.decrypt(cipherTextBlob, secretManagerKmsKey); - } - - return { - encryptor: ({ plainText }: { plainText: Buffer }) => { - const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey); - - // Buffer#1 encrypted text + Buffer#2 version number - const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 - const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]); - return { cipherTextBlob }; - }, - decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: { cipherTextBlob: Buffer }) => { - const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); - const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey); - return decryptedBlob; - } - }; -}; diff --git a/backend/src/db/migrations/utils/ring-buffer.ts b/backend/src/db/migrations/utils/ring-buffer.ts new file mode 100644 index 000000000..8e5c58662 --- /dev/null +++ b/backend/src/db/migrations/utils/ring-buffer.ts @@ -0,0 +1,19 @@ +export const createCircularCache = (bufferSize = 10) => { + const bufferItems: { id: string; item: T }[] = []; + let bufferIndex = 0; + + const push = (id: string, item: T) => { + if (bufferItems.length < bufferSize) { + bufferItems.push({ id, item }); + } else { + bufferItems[bufferIndex] = { id, item }; + } + bufferIndex = (bufferIndex + 1) % bufferSize; + }; + + const getItem = (id: string) => { + return bufferItems.find((i) => i.id === id)?.item; + }; + + return { push, getItem }; +}; diff --git a/backend/src/db/migrations/utils/services.ts b/backend/src/db/migrations/utils/services.ts new file mode 100644 index 000000000..731f703e2 --- /dev/null +++ b/backend/src/db/migrations/utils/services.ts @@ -0,0 +1,52 @@ +import { Knex } from "knex"; + +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; +import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; +import { kmsServiceFactory } from "@app/services/kms/kms-service"; +import { orgDALFactory } from "@app/services/org/org-dal"; +import { projectDALFactory } from "@app/services/project/project-dal"; + +import { TMigrationEnvConfig } from "./env-config"; + +type TDependencies = { + envConfig: TMigrationEnvConfig; + db: Knex; + keyStore: TKeyStoreFactory; +}; + +export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => { + // eslint-disable-next-line no-param-reassign + const hsmModule = initializeHsmModule(envConfig); + hsmModule.initialize(); + + const hsmService = hsmServiceFactory({ + hsmModule: hsmModule.getModule(), + envConfig + }); + + const orgDAL = orgDALFactory(db); + const kmsRootConfigDAL = kmsRootConfigDALFactory(db); + const kmsDAL = kmskeyDALFactory(db); + const internalKmsDAL = internalKmsDALFactory(db); + const projectDAL = projectDALFactory(db); + + const kmsService = kmsServiceFactory({ + kmsRootConfigDAL, + keyStore, + kmsDAL, + internalKmsDAL, + orgDAL, + projectDAL, + hsmService, + envConfig + }); + + await hsmService.startService(); + await kmsService.startService(); + + return { kmsService }; +}; diff --git a/backend/src/db/rename-migrations-to-mjs.ts b/backend/src/db/rename-migrations-to-mjs.ts new file mode 100644 index 000000000..d09b5097d --- /dev/null +++ b/backend/src/db/rename-migrations-to-mjs.ts @@ -0,0 +1,56 @@ +import path from "node:path"; + +import dotenv from "dotenv"; + +import { initAuditLogDbConnection, initDbConnection } from "./instance"; + +const isProduction = process.env.NODE_ENV === "production"; + +// Update with your config settings. . +dotenv.config({ + path: path.join(__dirname, "../../../.env.migration") +}); +dotenv.config({ + path: path.join(__dirname, "../../../.env") +}); + +const runRename = async () => { + if (!isProduction) return; + const migrationTable = "infisical_migrations"; + const applicationDb = initDbConnection({ + dbConnectionUri: process.env.DB_CONNECTION_URI as string, + dbRootCert: process.env.DB_ROOT_CERT + }); + + const auditLogDb = process.env.AUDIT_LOGS_DB_CONNECTION_URI + ? initAuditLogDbConnection({ + dbConnectionUri: process.env.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: process.env.AUDIT_LOGS_DB_ROOT_CERT + }) + : undefined; + + const hasMigrationTable = await applicationDb.schema.hasTable(migrationTable); + if (hasMigrationTable) { + const firstFile = (await applicationDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await applicationDb(migrationTable).update({ + name: applicationDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + if (auditLogDb) { + const hasMigrationTableInAuditLog = await auditLogDb.schema.hasTable(migrationTable); + if (hasMigrationTableInAuditLog) { + const firstFile = (await auditLogDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await auditLogDb(migrationTable).update({ + name: auditLogDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + } + await applicationDb.destroy(); + await auditLogDb?.destroy(); +}; + +void runRename(); diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index b27da396c..eaddea8fe 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -14,16 +16,17 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), - inputIV: z.string(), - inputCiphertext: z.string(), - inputTag: z.string(), + inputIV: z.string().nullable().optional(), + inputCiphertext: z.string().nullable().optional(), + inputTag: z.string().nullable().optional(), algorithm: z.string().default("aes-256-gcm"), keyEncoding: z.string().default("utf8"), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedInput: zodBuffer }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index ed99dec86..85f210ff1 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const IdentityKubernetesAuthsSchema = z.object({ @@ -17,15 +19,17 @@ export const IdentityKubernetesAuthsSchema = z.object({ updatedAt: z.date(), identityId: z.string().uuid(), kubernetesHost: z.string(), - encryptedCaCert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), - encryptedTokenReviewerJwt: z.string(), - tokenReviewerJwtIV: z.string(), - tokenReviewerJwtTag: z.string(), + encryptedCaCert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), + encryptedTokenReviewerJwt: z.string().nullable().optional(), + tokenReviewerJwtIV: z.string().nullable().optional(), + tokenReviewerJwtTag: z.string().nullable().optional(), allowedNamespaces: z.string(), allowedNames: z.string(), - allowedAudience: z.string() + allowedAudience: z.string(), + encryptedKubernetesTokenReviewerJwt: zodBuffer, + encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() }); export type TIdentityKubernetesAuths = z.infer; diff --git a/backend/src/db/schemas/identity-oidc-auths.ts b/backend/src/db/schemas/identity-oidc-auths.ts index 3d7d38c41..ebde5e7dc 100644 --- a/backend/src/db/schemas/identity-oidc-auths.ts +++ b/backend/src/db/schemas/identity-oidc-auths.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const IdentityOidcAuthsSchema = z.object({ @@ -15,15 +17,16 @@ export const IdentityOidcAuthsSchema = z.object({ accessTokenTrustedIps: z.unknown(), identityId: z.string().uuid(), oidcDiscoveryUrl: z.string(), - encryptedCaCert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), + encryptedCaCert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), boundIssuer: z.string(), boundAudiences: z.string(), boundClaims: z.unknown(), boundSubject: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedCaCertificate: zodBuffer.nullable().optional() }); export type TIdentityOidcAuths = z.infer; diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index dffaeec24..b56fab7bf 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -16,8 +16,7 @@ export const KmsKeysSchema = z.object({ name: z.string(), createdAt: z.date(), updatedAt: z.date(), - projectId: z.string().nullable().optional(), - slug: z.string().nullable().optional() + projectId: z.string().nullable().optional() }); export type TKmsKeys = z.infer; diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts index 460c2cff6..778e7be6e 100644 --- a/backend/src/db/schemas/ldap-configs.ts +++ b/backend/src/db/schemas/ldap-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const LdapConfigsSchema = z.object({ @@ -12,22 +14,25 @@ export const LdapConfigsSchema = z.object({ orgId: z.string().uuid(), isActive: z.boolean(), url: z.string(), - encryptedBindDN: z.string(), - bindDNIV: z.string(), - bindDNTag: z.string(), - encryptedBindPass: z.string(), - bindPassIV: z.string(), - bindPassTag: z.string(), + encryptedBindDN: z.string().nullable().optional(), + bindDNIV: z.string().nullable().optional(), + bindDNTag: z.string().nullable().optional(), + encryptedBindPass: z.string().nullable().optional(), + bindPassIV: z.string().nullable().optional(), + bindPassTag: z.string().nullable().optional(), searchBase: z.string(), - encryptedCACert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), + encryptedCACert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), groupSearchBase: z.string().default(""), groupSearchFilter: z.string().default(""), searchFilter: z.string().default(""), - uniqueUserAttribute: z.string().default("") + uniqueUserAttribute: z.string().default(""), + encryptedLdapBindDN: zodBuffer, + encryptedLdapBindPass: zodBuffer, + encryptedLdapCaCertificate: zodBuffer.nullable().optional() }); export type TLdapConfigs = z.infer; diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index d7bf2f00f..76923aee8 100644 --- a/backend/src/db/schemas/oidc-configs.ts +++ b/backend/src/db/schemas/oidc-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const OidcConfigsSchema = z.object({ @@ -15,20 +17,22 @@ export const OidcConfigsSchema = z.object({ jwksUri: z.string().nullable().optional(), tokenEndpoint: z.string().nullable().optional(), userinfoEndpoint: z.string().nullable().optional(), - encryptedClientId: z.string(), + encryptedClientId: z.string().nullable().optional(), configurationType: z.string(), - clientIdIV: z.string(), - clientIdTag: z.string(), - encryptedClientSecret: z.string(), - clientSecretIV: z.string(), - clientSecretTag: z.string(), + clientIdIV: z.string().nullable().optional(), + clientIdTag: z.string().nullable().optional(), + encryptedClientSecret: z.string().nullable().optional(), + clientSecretIV: z.string().nullable().optional(), + clientSecretTag: z.string().nullable().optional(), allowedEmailDomains: z.string().nullable().optional(), isActive: z.boolean(), createdAt: z.date(), updatedAt: z.date(), orgId: z.string().uuid(), lastUsed: z.date().nullable().optional(), - manageGroupMemberships: z.boolean().default(false) + manageGroupMemberships: z.boolean().default(false), + encryptedOidcClientId: zodBuffer, + encryptedOidcClientSecret: zodBuffer }); export type TOidcConfigs = z.infer; diff --git a/backend/src/db/schemas/saml-configs.ts b/backend/src/db/schemas/saml-configs.ts index 67171469a..350e84492 100644 --- a/backend/src/db/schemas/saml-configs.ts +++ b/backend/src/db/schemas/saml-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const SamlConfigsSchema = z.object({ @@ -23,7 +25,10 @@ export const SamlConfigsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), orgId: z.string().uuid(), - lastUsed: z.date().nullable().optional() + lastUsed: z.date().nullable().optional(), + encryptedSamlEntryPoint: zodBuffer, + encryptedSamlIssuer: zodBuffer, + encryptedSamlCertificate: zodBuffer }); export type TSamlConfigs = z.infer; diff --git a/backend/src/db/schemas/secret-rotations.ts b/backend/src/db/schemas/secret-rotations.ts index b491edc46..a3cd04ebb 100644 --- a/backend/src/db/schemas/secret-rotations.ts +++ b/backend/src/db/schemas/secret-rotations.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const SecretRotationsSchema = z.object({ @@ -22,7 +24,8 @@ export const SecretRotationsSchema = z.object({ keyEncoding: z.string().nullable().optional(), envId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedRotationData: zodBuffer }); export type TSecretRotations = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index a7aac2933..60f031fff 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,12 +5,14 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ id: z.string().uuid(), secretPath: z.string().default("/"), - url: z.string(), + url: z.string().nullable().optional(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), @@ -25,7 +27,9 @@ export const WebhooksSchema = z.object({ urlCipherText: z.string().nullable().optional(), urlIV: z.string().nullable().optional(), urlTag: z.string().nullable().optional(), - type: z.string().default("general").nullable().optional() + type: z.string().default("general").nullable().optional(), + encryptedPassKey: zodBuffer.nullable().optional(), + encryptedUrl: zodBuffer }); export type TWebhooks = z.infer; diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index 735ba632c..2057677cf 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -14,7 +14,7 @@ import { FastifyRequest } from "fastify"; import LdapStrategy from "passport-ldapauth"; import { z } from "zod"; -import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas"; +import { LdapGroupMapsSchema } from "@app/db/schemas"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { getConfig } from "@app/lib/config/env"; @@ -22,6 +22,7 @@ import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedLdapConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerLdapRouter = async (server: FastifyZodProvider) => { @@ -187,7 +188,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { caCert: z.string().trim().default("") }), response: { - 200: LdapConfigsSchema + 200: SanitizedLdapConfigSchema } }, handler: async (req) => { @@ -228,7 +229,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ organizationId: z.string() })), response: { - 200: LdapConfigsSchema + 200: SanitizedLdapConfigSchema } }, handler: async (req) => { diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index 71daa3446..df5c61fe4 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -11,13 +11,28 @@ import fastifySession from "@fastify/session"; import RedisStore from "connect-redis"; import { z } from "zod"; -import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs"; +import { OidcConfigsSchema } from "@app/db/schemas"; import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; import { getConfig } from "@app/lib/config/env"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({ + id: true, + issuer: true, + authorizationEndpoint: true, + configurationType: true, + discoveryURL: true, + jwksUri: true, + tokenEndpoint: true, + userinfoEndpoint: true, + orgId: true, + isActive: true, + allowedEmailDomains: true, + manageGroupMemberships: true +}); + export const registerOidcRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" }); @@ -142,7 +157,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { orgSlug: z.string().trim() }), response: { - 200: OidcConfigsSchema.pick({ + 200: SanitizedOidcConfigSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -214,7 +229,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ orgSlug: z.string() })), response: { - 200: OidcConfigsSchema.pick({ + 200: SanitizedOidcConfigSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -327,20 +342,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { } }), response: { - 200: OidcConfigsSchema.pick({ - id: true, - issuer: true, - authorizationEndpoint: true, - configurationType: true, - discoveryURL: true, - jwksUri: true, - tokenEndpoint: true, - userinfoEndpoint: true, - orgId: true, - isActive: true, - allowedEmailDomains: true, - manageGroupMemberships: true - }) + 200: SanitizedOidcConfigSchema } }, diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index 60f93d65d..cabf65337 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -9,7 +9,7 @@ import { ProjectTemplates } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { AuthMode } from "@app/services/auth/auth-type"; const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768; diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 933015a66..71facb22a 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -12,13 +12,13 @@ import { MultiSamlStrategy } from "@node-saml/passport-saml"; import { FastifyRequest } from "fastify"; import { z } from "zod"; -import { SamlConfigsSchema } from "@app/db/schemas"; import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config"; import { AuthMode } from "@app/services/auth/auth-type"; type TSAMLConfig = { @@ -298,7 +298,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { cert: z.string() }), response: { - 200: SamlConfigsSchema + 200: SanitizedSamlConfigSchema } }, handler: async (req) => { @@ -333,7 +333,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ organizationId: z.string() })), response: { - 200: SamlConfigsSchema + 200: SanitizedSamlConfigSchema } }, handler: async (req) => { diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index bb3e179dd..de37a4cde 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -9,7 +9,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege"; +import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/sanitizedSchema/user-additional-privilege"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index 7934c3f90..d9c3a05b5 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -9,7 +9,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege"; +import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchema/identitiy-additional-privilege"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 54834fe95..2c61d3c28 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -232,6 +232,7 @@ export enum EventType { UPDATE_CMEK = "update-cmek", DELETE_CMEK = "delete-cmek", GET_CMEKS = "get-cmeks", + GET_CMEK = "get-cmek", CMEK_ENCRYPT = "cmek-encrypt", CMEK_DECRYPT = "cmek-decrypt", UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping", @@ -355,6 +356,8 @@ interface GetSecretsEvent { }; } +type TSecretMetadata = { key: string; value: string }[]; + interface GetSecretEvent { type: EventType.GET_SECRET; metadata: { @@ -363,6 +366,7 @@ interface GetSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -374,6 +378,7 @@ interface CreateSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -382,7 +387,12 @@ interface CreateSecretBatchEvent { metadata: { environment: string; secretPath: string; - secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>; + secrets: Array<{ + secretId: string; + secretKey: string; + secretVersion: number; + secretMetadata?: TSecretMetadata; + }>; }; } @@ -394,6 +404,7 @@ interface UpdateSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -402,7 +413,7 @@ interface UpdateSecretBatchEvent { metadata: { environment: string; secretPath: string; - secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>; + secrets: Array<{ secretId: string; secretKey: string; secretVersion: number; secretMetadata?: TSecretMetadata }>; }; } @@ -1875,6 +1886,13 @@ interface GetCmeksEvent { }; } +interface GetCmekEvent { + type: EventType.GET_CMEK; + metadata: { + keyId: string; + }; +} + interface CmekEncryptEvent { type: EventType.CMEK_ENCRYPT; metadata: { @@ -2398,6 +2416,7 @@ export type Event = | CreateCmekEvent | UpdateCmekEvent | DeleteCmekEvent + | GetCmekEvent | GetCmeksEvent | CmekEncryptEvent | CmekDecryptEvent diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 810628030..e9f00f401 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -37,11 +37,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"), db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"), db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"), - db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"), - db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"), - db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"), - db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"), - db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"), + db.ref("encryptedInput").withSchema(TableName.DynamicSecret).as("dynEncryptedInput"), db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"), db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"), db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"), @@ -59,11 +55,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { type: doc.dynType, defaultTTL: doc.dynDefaultTTL, maxTTL: doc.dynMaxTTL, - inputIV: doc.dynInputIV, - inputTag: doc.dynInputTag, - inputCiphertext: doc.dynInputCiphertext, - algorithm: doc.dynAlgorithm, - keyEncoding: doc.dynKeyEncoding, + encryptedInput: doc.dynEncryptedInput, folderId: doc.dynFolderId, status: doc.dynStatus, statusDetails: doc.dynStatusDetails, diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index 9bdb1c24e..fa1a80ac3 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -1,8 +1,10 @@ -import { SecretKeyEncoding } from "@app/db/schemas"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; @@ -14,6 +16,8 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { dynamicSecretLeaseDAL: Pick; dynamicSecretDAL: Pick; dynamicSecretProviders: Record; + kmsService: Pick; + folderDAL: Pick; }; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType; @@ -22,7 +26,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, - dynamicSecretLeaseDAL + dynamicSecretLeaseDAL, + kmsService, + folderDAL }: TDynamicSecretLeaseQueueServiceFactoryDep) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queue( @@ -76,15 +82,21 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId); + if (!folder) + throw new NotFoundError({ + message: `Failed to find folder with ${dynamicSecretLease.dynamicSecret.folderId}` + }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: folder.projectId + }); + const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); @@ -100,16 +112,22 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) throw new DisableRotationErrors({ message: "Document not deleted" }); + const folder = await folderDAL.findById(dynamicSecretCfg.folderId); + if (!folder) + throw new NotFoundError({ + message: `Failed to find folder with ${dynamicSecretCfg.folderId}` + }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: folder.projectId + }); + const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId }); if (dynamicSecretLeases.length) { const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 830c1aa57..39e8ae7e2 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; -import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -9,9 +9,10 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -37,6 +38,7 @@ type TDynamicSecretLeaseServiceFactoryDep = { folderDAL: Pick; permissionService: Pick; projectDAL: Pick; + kmsService: Pick; }; export type TDynamicSecretLeaseServiceFactory = ReturnType; @@ -49,7 +51,8 @@ export const dynamicSecretLeaseServiceFactory = ({ permissionService, dynamicSecretQueueService, projectDAL, - licenseService + licenseService, + kmsService }: TDynamicSecretLeaseServiceFactoryDep) => { const create = async ({ environmentSlug, @@ -104,13 +107,14 @@ export const dynamicSecretLeaseServiceFactory = ({ throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; @@ -160,6 +164,11 @@ export const dynamicSecretLeaseServiceFactory = ({ subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const plan = await licenseService.getPlan(actorOrgId); if (!plan?.dynamicSecret) { throw new BadRequestError({ @@ -181,12 +190,7 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; @@ -240,6 +244,11 @@ export const dynamicSecretLeaseServiceFactory = ({ subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) throw new NotFoundError({ @@ -253,12 +262,7 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; const revokeResponse = await selectedProvider diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 631d5b6ba..eac5e2ecf 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,15 +1,16 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -42,6 +43,7 @@ type TDynamicSecretServiceFactoryDep = { folderDAL: Pick; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; }; export type TDynamicSecretServiceFactory = ReturnType; @@ -54,7 +56,8 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretProviders, permissionService, dynamicSecretQueueService, - projectDAL + projectDAL, + kmsService }: TDynamicSecretServiceFactoryDep) => { const create = async ({ path, @@ -108,16 +111,15 @@ export const dynamicSecretServiceFactory = ({ const isConnected = await selectedProvider.validateConnection(provider.inputs); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const dynamicSecretCfg = await dynamicSecretDAL.create({ type: provider.type, version: 1, - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, + encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob, maxTTL, defaultTTL, folderId: folder.id, @@ -180,15 +182,15 @@ export const dynamicSecretServiceFactory = ({ if (existingDynamicSecret) throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); } + const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const updatedInput = await selectedProvider.validateProviderInputs(newInput); @@ -196,13 +198,8 @@ export const dynamicSecretServiceFactory = ({ const isConnected = await selectedProvider.validateConnection(newInput); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput)); const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, + encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob, maxTTL, defaultTTL, name: newName ?? name, @@ -315,13 +312,13 @@ export const dynamicSecretServiceFactory = ({ if (!dynamicSecretCfg) { throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` }); } + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts index fc38a2a9b..59f82c05d 100644 --- a/backend/src/ee/services/group/group-dal.ts +++ b/backend/src/ee/services/group/group-dal.ts @@ -111,7 +111,7 @@ export const groupDALFactory = (db: TDbClient) => { } if (search) { - void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike '%${search}%'`); + void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike ?`, [`%${search}%`]); } else if (username) { void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`); } diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts index 3124e1012..ef975a371 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -1,25 +1,23 @@ import * as pkcs11js from "pkcs11js"; -import { getConfig } from "@app/lib/config/env"; +import { TEnvConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { HsmModule } from "./hsm-types"; -export const initializeHsmModule = () => { - const appCfg = getConfig(); - +export const initializeHsmModule = (envConfig: Pick) => { // Create a new instance of PKCS11 module const pkcs11 = new pkcs11js.PKCS11(); let isInitialized = false; const initialize = () => { - if (!appCfg.isHsmConfigured) { + if (!envConfig.isHsmConfigured) { return; } try { // Load the PKCS#11 module - pkcs11.load(appCfg.HSM_LIB_PATH!); + pkcs11.load(envConfig.HSM_LIB_PATH!); // Initialize the module pkcs11.C_Initialize(); diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index a1a0773fc..d35d17a24 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -1,12 +1,13 @@ import pkcs11js from "pkcs11js"; -import { getConfig } from "@app/lib/config/env"; +import { TEnvConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { HsmKeyType, HsmModule } from "./hsm-types"; type THsmServiceFactoryDep = { hsmModule: HsmModule; + envConfig: Pick; }; export type THsmServiceFactory = ReturnType; @@ -15,9 +16,7 @@ type SyncOrAsync = T | Promise; type SessionCallback = (session: pkcs11js.Handle) => SyncOrAsync; // eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => { - const appCfg = getConfig(); - +export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envConfig }: THsmServiceFactoryDep) => { // Constants for buffer structures const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc const BLOCK_SIZE = 16; @@ -63,11 +62,11 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm throw new Error("No slots available"); } - if (appCfg.HSM_SLOT >= slots.length) { - throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`); + if (envConfig.HSM_SLOT >= slots.length) { + throw new Error(`HSM slot ${envConfig.HSM_SLOT} not found or not initialized`); } - const slotId = slots[appCfg.HSM_SLOT]; + const slotId = slots[envConfig.HSM_SLOT]; const startTime = Date.now(); while (Date.now() - startTime < MAX_TIMEOUT) { @@ -78,7 +77,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm // Login try { - pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN); + pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, envConfig.HSM_PIN); logger.info("HSM: Successfully authenticated"); break; } catch (error) { @@ -86,7 +85,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm if (error instanceof pkcs11js.Pkcs11Error) { if (error.code === pkcs11js.CKR_PIN_INCORRECT) { // We throw instantly here to prevent further attempts, because if too many attempts are made, the HSM will potentially wipe all key material - logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`); + logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${envConfig.HSM_SLOT}`); throw new Error("HSM: Incorrect HSM Pin detected. Please check the HSM configuration."); } if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) { @@ -133,7 +132,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm }; const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => { - const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL; + const label = type === HsmKeyType.HMAC ? `${envConfig.HSM_KEY_LABEL}_HMAC` : envConfig.HSM_KEY_LABEL; const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES; const template = [ @@ -360,7 +359,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm }; const isActive = async () => { - if (!isInitialized || !appCfg.isHsmConfigured) { + if (!isInitialized || !envConfig.isHsmConfigured) { return false; } @@ -372,11 +371,11 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm logger.error(err, "HSM: Error testing PKCS#11 module"); } - return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed; + return envConfig.isHsmConfigured && isInitialized && pkcs11TestPassed; }; const startService = async () => { - if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return; + if (!envConfig.isHsmConfigured || !pkcs11 || !isInitialized) return; try { await $withSession(async (sessionHandle) => { @@ -395,7 +394,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES }, { type: pkcs11js.CKA_VALUE_LEN, value: AES_KEY_SIZE / 8 }, - { type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! }, + { type: pkcs11js.CKA_LABEL, value: envConfig.HSM_KEY_LABEL! }, { type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption { type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption ...genericAttributes @@ -410,7 +409,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm keyTemplate ); - logger.info(`HSM: Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); + logger.info(`HSM: Master key created successfully with label: ${envConfig.HSM_KEY_LABEL}`); } // Check if HMAC key exists, create if not @@ -419,7 +418,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET }, { type: pkcs11js.CKA_VALUE_LEN, value: HMAC_KEY_SIZE / 8 }, // 256-bit key - { type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` }, + { type: pkcs11js.CKA_LABEL, value: `${envConfig.HSM_KEY_LABEL!}_HMAC` }, { type: pkcs11js.CKA_SIGN, value: true }, // Allow signing { type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification ...genericAttributes @@ -434,7 +433,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm hmacKeyTemplate ); - logger.info(`HSM: HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`); + logger.info(`HSM: HMAC key created successfully with label: ${envConfig.HSM_KEY_LABEL}_HMAC`); } // Get slot info to check supported mechanisms diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 3a38c0d65..eb9c66c1c 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -5,7 +5,7 @@ import ms from "ms"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 16c0cc212..d74f9c504 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -5,7 +5,7 @@ import ms from "ms"; import { ActionProjectType } from "@app/db/schemas"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index cafc7abf0..e22b18e1b 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,25 +1,18 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -59,7 +52,6 @@ type TLdapConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - orgBotDAL: Pick; groupDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; @@ -84,6 +76,7 @@ type TLdapConfigServiceFactoryDep = { licenseService: Pick; tokenService: Pick; smtpService: Pick; + kmsService: Pick; }; export type TLdapConfigServiceFactory = ReturnType; @@ -93,7 +86,6 @@ export const ldapConfigServiceFactory = ({ ldapGroupMapDAL, orgDAL, orgMembershipDAL, - orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -105,7 +97,8 @@ export const ldapConfigServiceFactory = ({ permissionService, licenseService, tokenService, - smtpService + smtpService, + kmsService }: TLdapConfigServiceFactoryDep) => { const createLdapCfg = async ({ actor, @@ -133,77 +126,23 @@ export const ldapConfigServiceFactory = ({ message: "Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration." }); - - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); - const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const ldapConfig = await ldapConfigDAL.create({ orgId, isActive, url, - encryptedBindDN, - bindDNIV, - bindDNTag, - encryptedBindPass, - bindPassIV, - bindPassTag, uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, groupSearchFilter, - encryptedCACert, - caCertIV, - caCertTag + encryptedLdapCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, + encryptedLdapBindDN: encryptor({ plainText: Buffer.from(bindDN) }).cipherTextBlob, + encryptedLdapBindPass: encryptor({ plainText: Buffer.from(bindPass) }).cipherTextBlob }); return ldapConfig; @@ -246,38 +185,21 @@ export const ldapConfigServiceFactory = ({ uniqueUserAttribute }; - const orgBot = await orgBotDAL.findOne({ orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot in organization with ID '${orgId}' not found`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); if (bindDN !== undefined) { - const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); - updateQuery.encryptedBindDN = encryptedBindDN; - updateQuery.bindDNIV = bindDNIV; - updateQuery.bindDNTag = bindDNTag; + updateQuery.encryptedLdapBindDN = encryptor({ plainText: Buffer.from(bindDN) }).cipherTextBlob; } if (bindPass !== undefined) { - const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); - updateQuery.encryptedBindPass = encryptedBindPass; - updateQuery.bindPassIV = bindPassIV; - updateQuery.bindPassTag = bindPassTag; + updateQuery.encryptedLdapBindPass = encryptor({ plainText: Buffer.from(bindPass) }).cipherTextBlob; } if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCACert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedLdapCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } const [ldapConfig] = await ldapConfigDAL.update({ orgId }, updateQuery); @@ -293,61 +215,24 @@ export const ldapConfigServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found in organization with ID ${ldapConfig.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: ldapConfig.orgId }); - const { - encryptedBindDN, - bindDNIV, - bindDNTag, - encryptedBindPass, - bindPassIV, - bindPassTag, - encryptedCACert, - caCertIV, - caCertTag - } = ldapConfig; - let bindDN = ""; - if (encryptedBindDN && bindDNIV && bindDNTag) { - bindDN = decryptSymmetric({ - ciphertext: encryptedBindDN, - key, - tag: bindDNTag, - iv: bindDNIV - }); + if (ldapConfig.encryptedLdapBindDN) { + bindDN = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindDN }).toString(); } let bindPass = ""; - if (encryptedBindPass && bindPassIV && bindPassTag) { - bindPass = decryptSymmetric({ - ciphertext: encryptedBindPass, - key, - tag: bindPassTag, - iv: bindPassIV - }); + if (ldapConfig.encryptedLdapBindPass) { + bindPass = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindPass }).toString(); } let caCert = ""; - if (encryptedCACert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCACert, - key, - tag: caCertTag, - iv: caCertIV - }); + if (ldapConfig.encryptedLdapCaCertificate) { + caCert = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapCaCertificate }).toString(); } return { diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 0c037a2d3..52c8dd597 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; @@ -14,21 +14,14 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -70,7 +63,6 @@ type TOidcConfigServiceFactoryDep = { "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; orgMembershipDAL: Pick; - orgBotDAL: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; @@ -91,6 +83,7 @@ type TOidcConfigServiceFactoryDep = { projectDAL: Pick; projectBotDAL: Pick; auditLogService: Pick; + kmsService: Pick; }; export type TOidcConfigServiceFactory = ReturnType; @@ -103,7 +96,6 @@ export const oidcConfigServiceFactory = ({ licenseService, permissionService, tokenService, - orgBotDAL, smtpService, oidcConfigDAL, userGroupMembershipDAL, @@ -112,7 +104,8 @@ export const oidcConfigServiceFactory = ({ projectKeyDAL, projectDAL, projectBotDAL, - auditLogService + auditLogService, + kmsService }: TOidcConfigServiceFactoryDep) => { const getOidc = async (dto: TGetOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: dto.orgSlug }); @@ -143,43 +136,19 @@ export const oidcConfigServiceFactory = ({ }); } - // decrypt and return cfg - const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot for organization with ID '${oidcCfg.orgId}' not found`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: oidcCfg.orgId }); - const { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } = - oidcCfg; - let clientId = ""; - if (encryptedClientId && clientIdIV && clientIdTag) { - clientId = decryptSymmetric({ - ciphertext: encryptedClientId, - key, - tag: clientIdTag, - iv: clientIdIV - }); + if (oidcCfg.encryptedOidcClientId) { + clientId = decryptor({ cipherTextBlob: oidcCfg.encryptedOidcClientId }).toString(); } let clientSecret = ""; - if (encryptedClientSecret && clientSecretIV && clientSecretTag) { - clientSecret = decryptSymmetric({ - key, - tag: clientSecretTag, - iv: clientSecretIV, - ciphertext: encryptedClientSecret - }); + if (oidcCfg.encryptedOidcClientSecret) { + clientSecret = decryptor({ cipherTextBlob: oidcCfg.encryptedOidcClientSecret }).toString(); } return { @@ -540,12 +509,10 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); - const orgBot = await orgBotDAL.findOne({ orgId: org.id }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot for organization with ID '${org.id}' not found`, - name: "OrgBotNotFound" - }); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: org.id + }); const serverCfg = await getServerCfg(); if (isActive && !serverCfg.trustOidcEmails) { @@ -558,13 +525,6 @@ export const oidcConfigServiceFactory = ({ } } - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - const updateQuery: TOidcConfigsUpdate = { allowedEmailDomains, configurationType, @@ -580,22 +540,11 @@ export const oidcConfigServiceFactory = ({ }; if (clientId !== undefined) { - const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); - updateQuery.encryptedClientId = encryptedClientId; - updateQuery.clientIdIV = clientIdIV; - updateQuery.clientIdTag = clientIdTag; + updateQuery.encryptedOidcClientId = encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob; } if (clientSecret !== undefined) { - const { - ciphertext: encryptedClientSecret, - iv: clientSecretIV, - tag: clientSecretTag - } = encryptSymmetric(clientSecret, key); - - updateQuery.encryptedClientSecret = encryptedClientSecret; - updateQuery.clientSecretIV = clientSecretIV; - updateQuery.clientSecretTag = clientSecretTag; + updateQuery.encryptedOidcClientSecret = encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob; } const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); @@ -647,61 +596,11 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: org.id }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: org.id, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: org.id }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); - const { - ciphertext: encryptedClientSecret, - iv: clientSecretIV, - tag: clientSecretTag - } = encryptSymmetric(clientSecret, key); - const oidcCfg = await oidcConfigDAL.create({ issuer, isActive, @@ -713,13 +612,9 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, orgId: org.id, - encryptedClientId, - clientIdIV, - clientIdTag, - encryptedClientSecret, - clientSecretIV, - clientSecretTag, - manageGroupMemberships + manageGroupMemberships, + encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, + encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob }); return oidcCfg; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index b9a24889f..4389c3866 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -6,7 +6,7 @@ import { CASL_ACTION_SCHEMA_NATIVE_ENUM } from "@app/ee/services/permission/permission-schemas"; import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { PermissionConditionSchema } from "./permission-types"; diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index 5afa58caf..b2430ac14 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -15,7 +15,7 @@ import { } from "@app/ee/services/project-template/project-template-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; -import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { getPredefinedRoles } from "@app/services/project-role/project-role-fns"; import { TProjectTemplateDALFactory } from "./project-template-dal"; diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index 6b600f386..c2764dc53 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; export type TProjectTemplateEnvironment = Pick; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 14586d5e2..6f87663b2 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -5,7 +5,7 @@ import ms from "ms"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 068a45520..f22e2ad58 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -1,29 +1,15 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { - OrgMembershipStatus, - SecretKeyEncoding, - TableName, - TSamlConfigs, - TSamlConfigsUpdate, - TUsers -} from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TSamlConfigs, TSamlConfigsUpdate, TUsers } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -52,21 +38,19 @@ type TSamlConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - identityMetadataDAL: Pick; orgMembershipDAL: Pick; - orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; + kmsService: Pick; }; export type TSamlConfigServiceFactory = ReturnType; export const samlConfigServiceFactory = ({ samlConfigDAL, - orgBotDAL, orgDAL, orgMembershipDAL, userDAL, @@ -75,7 +59,8 @@ export const samlConfigServiceFactory = ({ licenseService, tokenService, smtpService, - identityMetadataDAL + identityMetadataDAL, + kmsService }: TSamlConfigServiceFactoryDep) => { const createSamlCfg = async ({ cert, @@ -99,70 +84,18 @@ export const samlConfigServiceFactory = ({ "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration." }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedEntryPoint, iv: entryPointIV, tag: entryPointTag } = encryptSymmetric(entryPoint, key); - const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); - const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); const samlConfig = await samlConfigDAL.create({ orgId, authProvider, isActive, - encryptedEntryPoint, - entryPointIV, - entryPointTag, - encryptedIssuer, - issuerIV, - issuerTag, - encryptedCert, - certIV, - certTag + encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob, + encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob, + encryptedSamlCertificate: encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob }); return samlConfig; @@ -190,40 +123,21 @@ export const samlConfigServiceFactory = ({ }); const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; - const orgBot = await orgBotDAL.findOne({ orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${orgId}'`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); if (entryPoint !== undefined) { - const { - ciphertext: encryptedEntryPoint, - iv: entryPointIV, - tag: entryPointTag - } = encryptSymmetric(entryPoint, key); - updateQuery.encryptedEntryPoint = encryptedEntryPoint; - updateQuery.entryPointIV = entryPointIV; - updateQuery.entryPointTag = entryPointTag; + updateQuery.encryptedSamlEntryPoint = encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob; } + if (issuer !== undefined) { - const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); - updateQuery.encryptedIssuer = encryptedIssuer; - updateQuery.issuerIV = issuerIV; - updateQuery.issuerTag = issuerTag; + updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob; } + if (cert !== undefined) { - const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); - updateQuery.encryptedCert = encryptedCert; - updateQuery.certIV = certIV; - updateQuery.certTag = certTag; + updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob; } const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); @@ -233,14 +147,14 @@ export const samlConfigServiceFactory = ({ }; const getSaml = async (dto: TGetSamlCfgDTO) => { - let ssoConfig: TSamlConfigs | undefined; + let samlConfig: TSamlConfigs | undefined; if (dto.type === "org") { - ssoConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); - if (!ssoConfig) return; + samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); + if (!samlConfig) return; } else if (dto.type === "orgSlug") { const org = await orgDAL.findOne({ slug: dto.orgSlug }); if (!org) return; - ssoConfig = await samlConfigDAL.findOne({ orgId: org.id }); + samlConfig = await samlConfigDAL.findOne({ orgId: org.id }); } else if (dto.type === "ssoId") { // TODO: // We made this change because saml config ids were not moved over during the migration @@ -259,81 +173,51 @@ export const samlConfigServiceFactory = ({ const id = UUIDToMongoId[dto.id] ?? dto.id; - ssoConfig = await samlConfigDAL.findById(id); + samlConfig = await samlConfigDAL.findById(id); } - if (!ssoConfig) throw new NotFoundError({ message: `Failed to find SSO data` }); + if (!samlConfig) throw new NotFoundError({ message: `Failed to find SSO data` }); // when dto is type id means it's internally used if (dto.type === "org") { const { permission } = await permissionService.getOrgPermission( dto.actor, dto.actorId, - ssoConfig.orgId, + samlConfig.orgId, dto.actorAuthMethod, dto.actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } - const { - entryPointTag, - entryPointIV, - encryptedEntryPoint, - certTag, - certIV, - encryptedCert, - issuerTag, - issuerIV, - encryptedIssuer - } = ssoConfig; - - const orgBot = await orgBotDAL.findOne({ orgId: ssoConfig.orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found in organization with ID '${ssoConfig.orgId}'`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: samlConfig.orgId }); let entryPoint = ""; - if (encryptedEntryPoint && entryPointIV && entryPointTag) { - entryPoint = decryptSymmetric({ - ciphertext: encryptedEntryPoint, - key, - tag: entryPointTag, - iv: entryPointIV - }); + if (samlConfig.encryptedSamlEntryPoint) { + entryPoint = decryptor({ cipherTextBlob: samlConfig.encryptedSamlEntryPoint }).toString(); } let issuer = ""; - if (encryptedIssuer && issuerTag && issuerIV) { - issuer = decryptSymmetric({ - key, - tag: issuerTag, - iv: issuerIV, - ciphertext: encryptedIssuer - }); + if (samlConfig.encryptedSamlIssuer) { + issuer = decryptor({ cipherTextBlob: samlConfig.encryptedSamlIssuer }).toString(); } let cert = ""; - if (encryptedCert && certTag && certIV) { - cert = decryptSymmetric({ key, tag: certTag, iv: certIV, ciphertext: encryptedCert }); + if (samlConfig.encryptedSamlCertificate) { + cert = decryptor({ cipherTextBlob: samlConfig.encryptedSamlCertificate }).toString(); } return { - id: ssoConfig.id, - organization: ssoConfig.orgId, - orgId: ssoConfig.orgId, - authProvider: ssoConfig.authProvider, - isActive: ssoConfig.isActive, + id: samlConfig.id, + organization: samlConfig.orgId, + orgId: samlConfig.orgId, + authProvider: samlConfig.authProvider, + isActive: samlConfig.isActive, entryPoint, issuer, cert, - lastUsed: ssoConfig.lastUsed + lastUsed: samlConfig.lastUsed }; }; diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 355507ecf..fdc493b9f 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -5,13 +5,9 @@ import { IAMClient } from "@aws-sdk/client-iam"; -import { SecretKeyEncoding, SecretType } from "@app/db/schemas"; +import { SecretType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { - encryptSymmetric128BitHexKeyUTF8, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; +import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -135,20 +131,15 @@ export const secretRotationQueueFactory = ({ // deep copy const provider = JSON.parse(JSON.stringify(rotationProvider)) as TSecretRotationProviderTemplate; + const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: secretRotation.projectId + }); - // now get the encrypted variable values - // in includes the inputs, the previous outputs - // internal mapping variables etc - const { encryptedDataTag, encryptedDataIV, encryptedData, keyEncoding } = secretRotation; - if (!encryptedDataTag || !encryptedDataIV || !encryptedData || !keyEncoding) { - throw new DisableRotationErrors({ message: "No inputs found" }); - } - const decryptedData = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: encryptedData, - iv: encryptedDataIV, - tag: encryptedDataTag - }); + const decryptedData = secretManagerDecryptor({ + cipherTextBlob: secretRotation.encryptedRotationData + }).toString(); const variables = JSON.parse(decryptedData) as TSecretRotationEncData; // rotation set cycle @@ -303,11 +294,9 @@ export const secretRotationQueueFactory = ({ outputs: newCredential.outputs, internal: newCredential.internal }); - const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables)); - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId: secretRotation.projectId - }); + const encryptedRotationData = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(variables)) + }).cipherTextBlob; const numberOfSecretsRotated = rotationOutputs.length; if (shouldUseSecretV2Bridge) { @@ -323,11 +312,7 @@ export const secretRotationQueueFactory = ({ await secretRotationDAL.updateById( rotationId, { - encryptedData: encVarData.ciphertext, - encryptedDataIV: encVarData.iv, - encryptedDataTag: encVarData.tag, - keyEncoding: encVarData.encoding, - algorithm: encVarData.algorithm, + encryptedRotationData, lastRotatedAt: new Date(), statusMessage: "Rotated successfull", status: "success" @@ -371,11 +356,7 @@ export const secretRotationQueueFactory = ({ await secretRotationDAL.updateById( rotationId, { - encryptedData: encVarData.ciphertext, - encryptedDataIV: encVarData.iv, - encryptedDataTag: encVarData.tag, - keyEncoding: encVarData.encoding, - algorithm: encVarData.algorithm, + encryptedRotationData, lastRotatedAt: new Date(), statusMessage: "Rotated successfull", status: "success" diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index c456e1581..02da4b7ea 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -2,9 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import Ajv from "ajv"; import { ActionProjectType, ProjectVersion, TableName } from "@app/db/schemas"; -import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; @@ -30,6 +32,7 @@ type TSecretRotationServiceFactoryDep = { permissionService: Pick; secretRotationQueue: TSecretRotationQueueFactory; projectBotService: Pick; + kmsService: Pick; }; export type TSecretRotationServiceFactory = ReturnType; @@ -44,7 +47,8 @@ export const secretRotationServiceFactory = ({ folderDAL, secretDAL, projectBotService, - secretV2BridgeDAL + secretV2BridgeDAL, + kmsService }: TSecretRotationServiceFactoryDep) => { const getProviderTemplates = async ({ actor, @@ -156,7 +160,11 @@ export const secretRotationServiceFactory = ({ inputs: formattedInputs, creds: [] }; - const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData)); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const secretRotation = await secretRotationDAL.transaction(async (tx) => { const doc = await secretRotationDAL.create( { @@ -164,11 +172,8 @@ export const secretRotationServiceFactory = ({ secretPath, interval, envId: folder.envId, - encryptedDataTag: encData.tag, - encryptedDataIV: encData.iv, - encryptedData: encData.ciphertext, - algorithm: encData.algorithm, - keyEncoding: encData.encoding + encryptedRotationData: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(unencryptedData)) }) + .cipherTextBlob }, tx ); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 2e4ed0f93..1c34f6b3d 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ +// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index 8a9eeab8c..d8240f27e 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-await-in-loop */ +/* eslint-disable no-await-in-loop,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ import { Knex } from "knex"; import { z } from "zod"; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index dbfdfd063..a5f3c24c0 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -2,6 +2,12 @@ import { Redis } from "ioredis"; import { Redlock, Settings } from "@app/lib/red-lock"; +export enum PgSqlLock { + BootUpMigration = 2023, + SuperAdminInit = 2024, + KmsRootKeyInit = 2025 +} + export type TKeyStoreFactory = ReturnType; // all the key prefixes used must be set here to avoid conflict diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts new file mode 100644 index 000000000..1fe78cf7e --- /dev/null +++ b/backend/src/keystore/memory.ts @@ -0,0 +1,38 @@ +import { Lock } from "@app/lib/red-lock"; + +import { TKeyStoreFactory } from "./keystore"; + +export const inMemoryKeyStore = (): TKeyStoreFactory => { + const store: Record = {}; + + return { + setItem: async (key, value) => { + store[key] = value; + return "OK"; + }, + setItemWithExpiry: async (key, value) => { + store[key] = value; + return "OK"; + }, + deleteItem: async (key) => { + delete store[key]; + return 1; + }, + getItem: async (key) => { + const value = store[key]; + if (typeof value === "string") { + return value; + } + return null; + }, + incrementBy: async () => { + return 1; + }, + acquireLock: () => { + return Promise.resolve({ + release: () => {} + }) as Promise; + }, + waitTillReady: async () => {} + }; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index eca11e983..23f783792 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -688,7 +688,9 @@ export const RAW_SECRETS = { environment: "The slug of the environment to list secrets from.", secretPath: "The secret path to list secrets from.", includeImports: "Weather to include imported secrets or not.", - tagSlugs: "The comma separated tag slugs to filter secrets." + tagSlugs: "The comma separated tag slugs to filter secrets.", + metadataFilter: + "The secret metadata key-value pairs to filter secrets by. When querying for multiple metadata pairs, the query is treated as an AND operation. Secret metadata format is key=value1,value=value2|key=value3,value=value4." }, CREATE: { secretName: "The name of the secret to create.", @@ -1591,6 +1593,13 @@ export const KMS = { orderDirection: "The direction to order keys in.", search: "The text string to filter key names by." }, + GET_KEY_BY_ID: { + keyId: "The ID of the KMS key to retrieve." + }, + GET_KEY_BY_NAME: { + keyName: "The name of the KMS key to retrieve.", + projectId: "The ID of the project the key belongs to." + }, ENCRYPT: { keyId: "The ID of the key to encrypt the data with.", plaintext: "The plaintext to be encrypted (base64 encoded)." @@ -1709,21 +1718,40 @@ export const SecretSyncs = { SYNC_OPTIONS: (destination: SecretSync) => { const destinationName = SECRET_SYNC_NAME_MAP[destination]; return { - INITIAL_SYNC_BEHAVIOR: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`, - PREPEND_PREFIX: `Optionally prepend a prefix to your secrets' keys when syncing to ${destinationName}.`, - APPEND_SUFFIX: `Optionally append a suffix to your secrets' keys when syncing to ${destinationName}.` + initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.` }; }, DESTINATION_CONFIG: { AWS_PARAMETER_STORE: { - REGION: "The AWS region to sync secrets to.", - PATH: "The Parameter Store path to sync secrets to." + region: "The AWS region to sync secrets to.", + path: "The Parameter Store path to sync secrets to." + }, + AWS_SECRETS_MANAGER: { + region: "The AWS region to sync secrets to.", + mappingBehavior: "How secrets from Infisical should be mapped to AWS Secrets Manager; one-to-one or many-to-one.", + secretName: "The secret name in AWS Secrets Manager to sync to when using mapping behavior many-to-one." }, GITHUB: { - ORG: "The name of the GitHub organization.", - OWNER: "The name of the GitHub account owner of the repository.", - REPO: "The name of the GitHub repository.", - ENV: "The name of the GitHub environment." + scope: "The GitHub scope that secrets should be synced to", + org: "The name of the GitHub organization.", + owner: "The name of the GitHub account owner of the repository.", + repo: "The name of the GitHub repository.", + env: "The name of the GitHub environment." + }, + AZURE_KEY_VAULT: { + vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/" + }, + AZURE_APP_CONFIGURATION: { + configurationUrl: + "The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/", + label: "An optional label to assign to secrets created in Azure App Configuration." + }, + GCP: { + scope: "The Google project scope that secrets should be synced to.", + projectId: "The ID of the Google project secrets should be synced to." + }, + DATABRICKS: { + scope: "The Databricks secret scope that secrets should be synced to." } } }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 627ccb053..801e937a2 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -204,6 +204,10 @@ const envSchema = z // gcp app INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()), + // azure app + INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()), + /* CORS ----------------------------------------------------------------------------- */ CORS_ALLOWED_ORIGINS: zpStr( @@ -254,7 +258,8 @@ const envSchema = z SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") })); -let envCfg: Readonly>; +export type TEnvConfig = Readonly>; +let envCfg: TEnvConfig; export const getConfig = () => envCfg; // cannot import singleton logger directly as it needs config to load various transport diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index 0022ee8ea..d43d2af8e 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -7,6 +7,7 @@ import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic"; export * from "./connection"; export * from "./join"; +export * from "./prependTableNameToFindFilter"; export * from "./select"; export const withTransaction = (db: Knex, dal: K) => ({ diff --git a/backend/src/lib/knex/prependTableNameToFindFilter.ts b/backend/src/lib/knex/prependTableNameToFindFilter.ts new file mode 100644 index 000000000..ee48dce5a --- /dev/null +++ b/backend/src/lib/knex/prependTableNameToFindFilter.ts @@ -0,0 +1,13 @@ +import { TableName } from "@app/db/schemas"; +import { buildFindFilter } from "@app/lib/knex/index"; + +type TFindFilterParameters = Parameters>[0]; + +export const prependTableNameToFindFilter = (tableName: TableName, filterObj: object): TFindFilterParameters => + Object.fromEntries( + Object.entries(filterObj).map(([key, value]) => + key.startsWith("$") + ? [key, prependTableNameToFindFilter(tableName, value as object)] + : [`${tableName}.${key}`, value] + ) + ); diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts index 9676496f7..170a0285f 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -98,7 +98,7 @@ const extractReqId = () => { } }; -export const initLogger = async () => { +export const initLogger = () => { const cfg = loggerConfig.parse(process.env); const targets: pino.TransportMultiOptions["targets"][number][] = [ { diff --git a/backend/src/main.ts b/backend/src/main.ts index 850298f89..461601fc0 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,14 +2,13 @@ import "./lib/telemetry/instrumentation"; import dotenv from "dotenv"; import { Redis } from "ioredis"; -import path from "path"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { runMigrations } from "./auto-start-migrations"; import { initAuditLogDbConnection, initDbConnection } from "./db"; import { keyStoreFactory } from "./keystore/keystore"; -import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env"; -import { isMigrationMode } from "./lib/fn"; +import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; @@ -19,58 +18,53 @@ import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); const run = async () => { - const logger = await initLogger(); - const appCfg = initEnvConfig(logger); + const logger = initLogger(); + const envConfig = initEnvConfig(logger); const db = initDbConnection({ - dbConnectionUri: appCfg.DB_CONNECTION_URI, - dbRootCert: appCfg.DB_ROOT_CERT, - readReplicas: appCfg.DB_READ_REPLICAS?.map((el) => ({ + dbConnectionUri: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT, + readReplicas: envConfig.DB_READ_REPLICAS?.map((el) => ({ dbRootCert: el.DB_ROOT_CERT, dbConnectionUri: el.DB_CONNECTION_URI })) }); - const auditLogDb = appCfg.AUDIT_LOGS_DB_CONNECTION_URI + const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI ? initAuditLogDbConnection({ - dbConnectionUri: appCfg.AUDIT_LOGS_DB_CONNECTION_URI, - dbRootCert: appCfg.AUDIT_LOGS_DB_ROOT_CERT + dbConnectionUri: envConfig.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: envConfig.AUDIT_LOGS_DB_ROOT_CERT }) : undefined; - // Case: App is running in packaged mode (binary), and migration mode is enabled. - // Run the migrations and exit the process after completion. - if (IS_PACKAGED && isMigrationMode()) { - try { - logger.info("Running Postgres migrations.."); - await db.migrate.latest({ - directory: path.join(__dirname, "./db/migrations") - }); - logger.info("Postgres migrations completed"); - } catch (err) { - logger.error(err, "Failed to run migrations"); - process.exit(1); - } - - process.exit(0); - } + await runMigrations({ applicationDb: db, auditLogDb, logger }); const smtp = smtpServiceFactory(formatSmtpConfig()); - const queue = queueServiceFactory(appCfg.REDIS_URL, { - dbConnectionUrl: appCfg.DB_CONNECTION_URI, - dbRootCert: appCfg.DB_ROOT_CERT + const queue = queueServiceFactory(envConfig.REDIS_URL, { + dbConnectionUrl: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT }); await queue.initialize(); - const keyStore = keyStoreFactory(appCfg.REDIS_URL); - const redis = new Redis(appCfg.REDIS_URL); + const keyStore = keyStoreFactory(envConfig.REDIS_URL); + const redis = new Redis(envConfig.REDIS_URL); - const hsmModule = initializeHsmModule(); + const hsmModule = initializeHsmModule(envConfig); hsmModule.initialize(); - const server = await main({ db, auditLogDb, hsmModule: hsmModule.getModule(), smtp, logger, queue, keyStore, redis }); + const server = await main({ + db, + auditLogDb, + hsmModule: hsmModule.getModule(), + smtp, + logger, + queue, + keyStore, + redis, + envConfig + }); const bootstrap = await bootstrapCheck({ db }); // eslint-disable-next-line @@ -90,8 +84,8 @@ const run = async () => { }); await server.listen({ - port: appCfg.PORT, - host: appCfg.HOST, + port: envConfig.PORT, + host: envConfig.HOST, listenTextResolver: (address) => { void bootstrap(); return address; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index ce1be4a04..26f556508 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -17,7 +17,7 @@ import { Knex } from "knex"; import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; +import { getConfig, IS_PACKAGED, TEnvConfig } from "@app/lib/config/env"; import { CustomLogger } from "@app/lib/logger/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TQueueServiceFactory } from "@app/queue"; @@ -43,10 +43,11 @@ type TMain = { keyStore: TKeyStoreFactory; hsmModule: HsmModule; redis: Redis; + envConfig: TEnvConfig; }; // Run the server! -export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore, redis }: TMain) => { +export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore, redis, envConfig }: TMain) => { const appCfg = getConfig(); const server = fastify({ @@ -127,7 +128,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key }) }); - await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule }); + await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule, envConfig }); await server.register(registerServeUI, { standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 09936467c..155b52cfb 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -91,7 +91,7 @@ import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certi import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { getConfig, TEnvConfig } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue"; @@ -250,7 +250,8 @@ export const registerRoutes = async ( hsmModule, smtp: smtpService, queue: queueService, - keyStore + keyStore, + envConfig }: { auditLogDb?: Knex; db: Knex; @@ -258,6 +259,7 @@ export const registerRoutes = async ( smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; + envConfig: TEnvConfig; } ) => { const appCfg = getConfig(); @@ -401,7 +403,8 @@ export const registerRoutes = async ( const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const hsmService = hsmServiceFactory({ - hsmModule + hsmModule, + envConfig }); const kmsService = kmsServiceFactory({ @@ -411,7 +414,8 @@ export const registerRoutes = async ( internalKmsDAL, orgDAL, projectDAL, - hsmService + hsmService, + envConfig }); const externalKmsService = externalKmsServiceFactory({ @@ -457,7 +461,6 @@ export const registerRoutes = async ( const samlService = samlConfigServiceFactory({ identityMetadataDAL, permissionService, - orgBotDAL, orgDAL, orgMembershipDAL, userDAL, @@ -465,7 +468,8 @@ export const registerRoutes = async ( samlConfigDAL, licenseService, tokenService, - smtpService + smtpService, + kmsService }); const groupService = groupServiceFactory({ userDAL, @@ -516,7 +520,6 @@ export const registerRoutes = async ( ldapGroupMapDAL, orgDAL, orgMembershipDAL, - orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -528,7 +531,8 @@ export const registerRoutes = async ( permissionService, licenseService, tokenService, - smtpService + smtpService, + kmsService }); const telemetryService = telemetryServiceFactory({ @@ -859,7 +863,8 @@ export const registerRoutes = async ( secretVersionTagDAL, secretVersionV2BridgeDAL, secretVersionTagV2BridgeDAL, - resourceMetadataDAL + resourceMetadataDAL, + appConnectionDAL }); const secretQueueService = secretQueueFactory({ @@ -978,7 +983,8 @@ export const registerRoutes = async ( permissionService, webhookDAL, projectEnvDAL, - projectDAL + projectDAL, + kmsService }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -1158,7 +1164,8 @@ export const registerRoutes = async ( secretDAL, folderDAL, projectBotService, - secretV2BridgeDAL + secretV2BridgeDAL, + kmsService }); const integrationService = integrationServiceFactory({ @@ -1247,9 +1254,9 @@ export const registerRoutes = async ( identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, - orgBotDAL, permissionService, - licenseService + licenseService, + kmsService }); const identityGcpAuthService = identityGcpAuthServiceFactory({ identityGcpAuthDAL, @@ -1281,7 +1288,7 @@ export const registerRoutes = async ( identityAccessTokenDAL, permissionService, licenseService, - orgBotDAL + kmsService }); const identityJwtAuthService = identityJwtAuthServiceFactory({ @@ -1298,7 +1305,9 @@ export const registerRoutes = async ( queueService, dynamicSecretLeaseDAL, dynamicSecretProviders, - dynamicSecretDAL + dynamicSecretDAL, + folderDAL, + kmsService }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -1308,7 +1317,8 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, permissionService, - licenseService + licenseService, + kmsService }); const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ projectDAL, @@ -1318,7 +1328,8 @@ export const registerRoutes = async ( dynamicSecretLeaseDAL, dynamicSecretProviders, folderDAL, - licenseService + licenseService, + kmsService }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, @@ -1346,7 +1357,7 @@ export const registerRoutes = async ( licenseService, tokenService, smtpService, - orgBotDAL, + kmsService, permissionService, oidcConfigDAL, projectBotDAL, diff --git a/backend/src/server/routes/sanitizedSchema/directory-config.ts b/backend/src/server/routes/sanitizedSchema/directory-config.ts new file mode 100644 index 000000000..61be4d9cf --- /dev/null +++ b/backend/src/server/routes/sanitizedSchema/directory-config.ts @@ -0,0 +1,42 @@ +import { LdapConfigsSchema, OidcConfigsSchema, SamlConfigsSchema } from "@app/db/schemas"; + +export const SanitizedSamlConfigSchema = SamlConfigsSchema.pick({ + id: true, + orgId: true, + isActive: true, + lastUsed: true, + createdAt: true, + updatedAt: true, + authProvider: true +}); + +export const SanitizedLdapConfigSchema = LdapConfigsSchema.pick({ + updatedAt: true, + createdAt: true, + isActive: true, + orgId: true, + id: true, + url: true, + searchBase: true, + searchFilter: true, + groupSearchBase: true, + uniqueUserAttribute: true, + groupSearchFilter: true +}); + +export const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({ + id: true, + orgId: true, + isActive: true, + createdAt: true, + updatedAt: true, + lastUsed: true, + issuer: true, + jwksUri: true, + discoveryURL: true, + tokenEndpoint: true, + userinfoEndpoint: true, + configurationType: true, + allowedEmailDomains: true, + authorizationEndpoint: true +}); diff --git a/backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts b/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts rename to backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts diff --git a/backend/src/server/routes/santizedSchemas/permission.ts b/backend/src/server/routes/sanitizedSchema/permission.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/permission.ts rename to backend/src/server/routes/sanitizedSchema/permission.ts diff --git a/backend/src/server/routes/santizedSchemas/user-additional-privilege.ts b/backend/src/server/routes/sanitizedSchema/user-additional-privilege.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/user-additional-privilege.ts rename to backend/src/server/routes/sanitizedSchema/user-additional-privilege.ts diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 67f01c9ba..4d645ac4b 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -11,7 +11,7 @@ import { } from "@app/db/schemas"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { UnpackedPermissionSchema } from "./santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "./sanitizedSchema/permission"; // sometimes the return data must be santizied to avoid leaking important values // always prefer pick over omit in zod @@ -201,10 +201,11 @@ export const SanitizedRoleSchemaV1 = ProjectRolesSchema.extend({ }); export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ + encryptedInput: true, + keyEncoding: true, + inputCiphertext: true, inputIV: true, inputTag: true, - inputCiphertext: true, - keyEncoding: true, algorithm: true }); diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts index 41a87feb5..e23d52004 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -73,7 +73,13 @@ export const registerAppConnectionEndpoints = { diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts new file mode 100644 index 000000000..3f3ca7a1a --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAzureAppConfigurationConnectionSchema, + SanitizedAzureAppConfigurationConnectionSchema, + UpdateAzureAppConfigurationConnectionSchema +} from "@app/services/app-connection/azure-app-configuration"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAzureAppConfigurationConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.AzureAppConfiguration, + server, + sanitizedResponseSchema: SanitizedAzureAppConfigurationConnectionSchema, + createSchema: CreateAzureAppConfigurationConnectionSchema, + updateSchema: UpdateAzureAppConfigurationConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts new file mode 100644 index 000000000..7097ed98b --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAzureKeyVaultConnectionSchema, + SanitizedAzureKeyVaultConnectionSchema, + UpdateAzureKeyVaultConnectionSchema +} from "@app/services/app-connection/azure-key-vault"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAzureKeyVaultConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.AzureKeyVault, + server, + sanitizedResponseSchema: SanitizedAzureKeyVaultConnectionSchema, + createSchema: CreateAzureKeyVaultConnectionSchema, + updateSchema: UpdateAzureKeyVaultConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts new file mode 100644 index 000000000..7fdb7f3a2 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateDatabricksConnectionSchema, + SanitizedDatabricksConnectionSchema, + UpdateDatabricksConnectionSchema +} from "@app/services/app-connection/databricks"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerDatabricksConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Databricks, + server, + sanitizedResponseSchema: SanitizedDatabricksConnectionSchema, + createSchema: CreateDatabricksConnectionSchema, + updateSchema: UpdateDatabricksConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/secret-scopes`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + secretScopes: z.object({ name: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const secretScopes = await server.services.appConnection.databricks.listSecretScopes( + connectionId, + req.permission + ); + + return { secretScopes }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts index 9c33f3fad..8444b0cd6 100644 --- a/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts @@ -41,7 +41,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { connectionId } = req.params; @@ -67,7 +67,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { connectionId } = req.params; @@ -97,7 +97,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { connectionId } = req.params; const { repo, owner } = req.query; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 4551a0fbb..e86c37753 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -1,6 +1,9 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { registerAwsConnectionRouter } from "./aws-connection-router"; +import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; +import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; +import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -10,5 +13,8 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { if (!isBase64(val)) { ctx.addIssue({ @@ -53,7 +57,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, @@ -106,7 +110,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, @@ -150,7 +154,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, @@ -201,7 +205,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - keys: KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).array(), + keys: CmekSchema.array(), totalCount: z.number() }) } @@ -230,6 +234,92 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/keys/:keyId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get KMS key by ID", + params: z.object({ + keyId: z.string().uuid().describe(KMS.GET_KEY_BY_ID.keyId) + }), + response: { + 200: z.object({ + key: CmekSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + permission + } = req; + + const key = await server.services.cmek.findCmekById(keyId, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: key.projectId!, + event: { + type: EventType.GET_CMEK, + metadata: { + keyId: key.id + } + } + }); + + return { key }; + } + }); + + server.route({ + method: "GET", + url: "/keys/key-name/:keyName", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get KMS key by Name", + params: z.object({ + keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName) + }), + querystring: z.object({ + projectId: z.string().min(1, "Project ID is required").describe(KMS.GET_KEY_BY_NAME.projectId) + }), + response: { + 200: z.object({ + key: CmekSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyName }, + query: { projectId }, + permission + } = req; + + const key = await server.services.cmek.findCmekByName(keyName, projectId, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: key.projectId!, + event: { + type: EventType.GET_CMEK, + metadata: { + keyId: key.id + } + } + }); + + return { key }; + } + }); + // encrypt data server.route({ method: "POST", diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 3b3025179..263fa478e 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -8,13 +8,19 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; -const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.omit({ - encryptedCaCert: true, - caCertIV: true, - caCertTag: true, - encryptedTokenReviewerJwt: true, - tokenReviewerJwtIV: true, - tokenReviewerJwtTag: true +const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({ + id: true, + accessTokenTTL: true, + accessTokenMaxTTL: true, + accessTokenNumUsesLimit: true, + accessTokenTrustedIps: true, + createdAt: true, + updatedAt: true, + identityId: true, + kubernetesHost: true, + allowedNamespaces: true, + allowedNames: true, + allowedAudience: true }).extend({ caCert: z.string(), tokenReviewerJwt: z.string() diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 431ed3f4f..7ce0b05b7 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -12,10 +12,20 @@ import { validateOidcBoundClaimsField } from "@app/services/identity-oidc-auth/identity-oidc-auth-validators"; -const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({ - encryptedCaCert: true, - caCertIV: true, - caCertTag: true +const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({ + id: true, + accessTokenTTL: true, + accessTokenMaxTTL: true, + accessTokenNumUsesLimit: true, + accessTokenTrustedIps: true, + identityId: true, + oidcDiscoveryUrl: true, + boundIssuer: true, + boundAudiences: true, + boundClaims: true, + boundSubject: true, + createdAt: true, + updatedAt: true }).extend({ caCert: z.string() }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts new file mode 100644 index 000000000..ff2cc5a84 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts @@ -0,0 +1,17 @@ +import { + AwsSecretsManagerSyncSchema, + CreateAwsSecretsManagerSyncSchema, + UpdateAwsSecretsManagerSyncSchema +} from "@app/services/secret-sync/aws-secrets-manager"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAwsSecretsManagerSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AWSSecretsManager, + server, + responseSchema: AwsSecretsManagerSyncSchema, + createSchema: CreateAwsSecretsManagerSyncSchema, + updateSchema: UpdateAwsSecretsManagerSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/azure-app-configuration-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/azure-app-configuration-sync-router.ts new file mode 100644 index 000000000..de9c0bd8b --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/azure-app-configuration-sync-router.ts @@ -0,0 +1,17 @@ +import { + AzureAppConfigurationSyncSchema, + CreateAzureAppConfigurationSyncSchema, + UpdateAzureAppConfigurationSyncSchema +} from "@app/services/secret-sync/azure-app-configuration"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAzureAppConfigurationSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AzureAppConfiguration, + server, + responseSchema: AzureAppConfigurationSyncSchema, + createSchema: CreateAzureAppConfigurationSyncSchema, + updateSchema: UpdateAzureAppConfigurationSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts new file mode 100644 index 000000000..a33c513c8 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts @@ -0,0 +1,17 @@ +import { + AzureKeyVaultSyncSchema, + CreateAzureKeyVaultSyncSchema, + UpdateAzureKeyVaultSyncSchema +} from "@app/services/secret-sync/azure-key-vault"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAzureKeyVaultSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AzureKeyVault, + server, + responseSchema: AzureKeyVaultSyncSchema, + createSchema: CreateAzureKeyVaultSyncSchema, + updateSchema: UpdateAzureKeyVaultSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/databricks-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/databricks-sync-router.ts new file mode 100644 index 000000000..b9c424a05 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/databricks-sync-router.ts @@ -0,0 +1,17 @@ +import { + CreateDatabricksSyncSchema, + DatabricksSyncSchema, + UpdateDatabricksSyncSchema +} from "@app/services/secret-sync/databricks"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerDatabricksSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Databricks, + server, + responseSchema: DatabricksSyncSchema, + createSchema: CreateDatabricksSyncSchema, + updateSchema: UpdateDatabricksSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 6b3d2f15f..137335186 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -1,6 +1,10 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router"; +import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router"; +import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router"; +import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; +import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; @@ -8,6 +12,10 @@ export * from "./secret-sync-router"; export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record Promise> = { [SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter, + [SecretSync.AWSSecretsManager]: registerAwsSecretsManagerSyncRouter, [SecretSync.GitHub]: registerGitHubSyncRouter, - [SecretSync.GCPSecretManager]: registerGcpSyncRouter + [SecretSync.GCPSecretManager]: registerGcpSyncRouter, + [SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter, + [SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter, + [SecretSync.Databricks]: registerDatabricksSyncRouter }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index 2703f8e44..120df4325 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -9,19 +9,37 @@ import { AwsParameterStoreSyncListItemSchema, AwsParameterStoreSyncSchema } from "@app/services/secret-sync/aws-parameter-store"; +import { + AwsSecretsManagerSyncListItemSchema, + AwsSecretsManagerSyncSchema +} from "@app/services/secret-sync/aws-secrets-manager"; +import { + AzureAppConfigurationSyncListItemSchema, + AzureAppConfigurationSyncSchema +} from "@app/services/secret-sync/azure-app-configuration"; +import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault"; +import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks"; import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp"; import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; const SecretSyncSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncSchema, + AwsSecretsManagerSyncSchema, GitHubSyncSchema, - GcpSyncSchema + GcpSyncSchema, + AzureKeyVaultSyncSchema, + AzureAppConfigurationSyncSchema, + DatabricksSyncSchema ]); const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncListItemSchema, + AwsSecretsManagerSyncListItemSchema, GitHubSyncListItemSchema, - GcpSyncListItemSchema + GcpSyncListItemSchema, + AzureKeyVaultSyncListItemSchema, + AzureAppConfigurationSyncListItemSchema, + DatabricksSyncListItemSchema ]); export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 67fd09ac8..af1747ed6 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -36,11 +36,12 @@ const SecretReferenceNodeTree: z.ZodType = SecretReference children: z.lazy(() => SecretReferenceNodeTree.array()) }); -const SecretNameSchema = z - .string() - .trim() - .min(1) - .refine((el) => !el.includes(" "), "Secret name cannot contain spaces."); +const BaseSecretNameSchema = z.string().trim().min(1); + +const SecretNameSchema = BaseSecretNameSchema.refine( + (el) => !el.includes(" "), + "Secret name cannot contain spaces." +).refine((el) => !el.includes(":"), "Secret name cannot contain colon."); export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ @@ -181,6 +182,66 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], querystring: z.object({ + metadataFilter: z + .string() + .optional() + .transform((val) => { + if (!val) return undefined; + + const result: { key?: string; value?: string }[] = []; + const pairs = val.split("|"); + + for (const pair of pairs) { + const keyValuePair: { key?: string; value?: string } = {}; + const parts = pair.split(/[,=]/); + + for (let i = 0; i < parts.length; i += 2) { + const identifier = parts[i].trim().toLowerCase(); + const value = parts[i + 1]?.trim(); + + if (identifier === "key" && value) { + keyValuePair.key = value; + } else if (identifier === "value" && value) { + keyValuePair.value = value; + } + } + + if (keyValuePair.key && keyValuePair.value) { + result.push(keyValuePair); + } + } + + return result.length ? result : undefined; + }) + .superRefine((metadata, ctx) => { + if (metadata && !Array.isArray(metadata)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Invalid secretMetadata format. Correct format is key=value1,value=value2|key=value3,value=value4." + }); + } + + if (metadata) { + if (metadata.length > 10) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "You can only filter by up to 10 metadata fields" + }); + } + + for (const item of metadata) { + if (!item.key && !item.value) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Invalid secretMetadata format, key or value must be provided. Correct format is key=value1,value=value2|key=value3,value=value4." + }); + } + } + } + }) + .describe(RAW_SECRETS.LIST.metadataFilter), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), @@ -281,6 +342,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, projectId: workspaceId, path: secretPath, + metadataFilter: req.query.metadataFilter, includeImports: req.query.include_imports, recursive: req.query.recursive, tagSlugs: req.query.tagSlugs @@ -411,7 +473,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: req.query.secretPath, secretId: secret.id, secretKey: req.params.secretName, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: secret.secretMetadata } } }); @@ -519,7 +582,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: req.body.secretPath, secretId: secret.id, secretKey: req.params.secretName, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: req.body.secretMetadata } } }); @@ -555,7 +619,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) + secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) }), body: z.object({ workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId), @@ -631,7 +695,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: req.body.secretPath, secretId: secret.id, secretKey: req.params.secretName, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: req.body.secretMetadata } } }); @@ -1904,6 +1969,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } const { secrets } = secretOperation; + const secretMetadataMap = new Map( + inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) + ); + await server.services.auditLog.createAuditLog({ projectId: secrets[0].workspace, ...req.auditLogInfo, @@ -1915,7 +1984,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secrets.map((secret) => ({ secretId: secret.id, secretKey: secret.secretKey, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) })) } } @@ -2010,6 +2080,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } const { secrets } = secretOperation; + const secretMetadataMap = new Map( + inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) + ); + await server.services.auditLog.createAuditLog({ projectId: secrets[0].workspace, ...req.auditLogInfo, @@ -2021,7 +2095,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secrets.map((secret) => ({ secretId: secret.id, secretKey: secret.secretKey, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) })) } } diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 61787b47c..ce116a80a 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -1,7 +1,10 @@ export enum AppConnection { GitHub = "github", AWS = "aws", - GCP = "gcp" + Databricks = "databricks", + GCP = "gcp", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index d4c9f97ad..c66fe52c4 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -5,12 +5,17 @@ import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/ap import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types"; import { AwsConnectionMethod, - getAwsAppConnectionListItem, + getAwsConnectionListItem, validateAwsConnectionCredentials } from "@app/services/app-connection/aws"; +import { + DatabricksConnectionMethod, + getDatabricksConnectionListItem, + validateDatabricksConnectionCredentials +} from "@app/services/app-connection/databricks"; import { GcpConnectionMethod, - getGcpAppConnectionListItem, + getGcpConnectionListItem, validateGcpConnectionCredentials } from "@app/services/app-connection/gcp"; import { @@ -20,10 +25,26 @@ import { } from "@app/services/app-connection/github"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { + AzureAppConfigurationConnectionMethod, + getAzureAppConfigurationConnectionListItem, + validateAzureAppConfigurationConnectionCredentials +} from "./azure-app-configuration"; +import { + AzureKeyVaultConnectionMethod, + getAzureKeyVaultConnectionListItem, + validateAzureKeyVaultConnectionCredentials +} from "./azure-key-vault"; + export const listAppConnectionOptions = () => { - return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) => - a.name.localeCompare(b.name) - ); + return [ + getAwsConnectionListItem(), + getGitHubConnectionListItem(), + getGcpConnectionListItem(), + getAzureKeyVaultConnectionListItem(), + getAzureAppConfigurationConnectionListItem(), + getDatabricksConnectionListItem() + ].sort((a, b) => a.name.localeCompare(b.name)); }; export const encryptAppConnectionCredentials = async ({ @@ -75,10 +96,16 @@ export const validateAppConnectionCredentials = async ( switch (app) { case AppConnection.AWS: return validateAwsConnectionCredentials(appConnection); + case AppConnection.Databricks: + return validateDatabricksConnectionCredentials(appConnection); case AppConnection.GitHub: return validateGitHubConnectionCredentials(appConnection); case AppConnection.GCP: return validateGcpConnectionCredentials(appConnection); + case AppConnection.AzureKeyVault: + return validateAzureKeyVaultConnectionCredentials(appConnection); + case AppConnection.AzureAppConfiguration: + return validateAzureAppConfigurationConnectionCredentials(appConnection); default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection ${app}`); @@ -89,6 +116,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => switch (method) { case GitHubConnectionMethod.App: return "GitHub App"; + case AzureKeyVaultConnectionMethod.OAuth: + case AzureAppConfigurationConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth: return "OAuth"; case AwsConnectionMethod.AccessKey: @@ -97,6 +126,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => return "Assume Role"; case GcpConnectionMethod.ServiceAccountImpersonation: return "Service Account Impersonation"; + case DatabricksConnectionMethod.ServicePrincipal: + return "Service Principal"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection Method: ${method}`); diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index abff5cf3b..8a045efe4 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -3,5 +3,8 @@ import { AppConnection } from "./app-connection-enums"; export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AWS]: "AWS", [AppConnection.GitHub]: "GitHub", - [AppConnection.GCP]: "GCP" + [AppConnection.GCP]: "GCP", + [AppConnection.AzureKeyVault]: "Azure Key Vault", + [AppConnection.AzureAppConfiguration]: "Azure App Configuration", + [AppConnection.Databricks]: "Databricks" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 8beaec21d..7ae8b6377 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -23,11 +23,15 @@ import { TValidateAppConnectionCredentials } from "@app/services/app-connection/app-connection-types"; import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws"; +import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks"; +import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service"; import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github"; import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TAppConnectionDALFactory } from "./app-connection-dal"; +import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; +import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { gcpConnectionService } from "./gcp/gcp-connection-service"; @@ -42,7 +46,10 @@ export type TAppConnectionServiceFactory = ReturnType = { [AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema, [AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema, - [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema + [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema, + [AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema, + [AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema, + [AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema }; export const appConnectionServiceFactory = ({ @@ -361,6 +368,7 @@ export const appConnectionServiceFactory = ({ connectAppConnectionById, listAvailableAppConnectionsForUser, github: githubConnectionService(connectAppConnectionById), - gcp: gcpConnectionService(connectAppConnectionById) + gcp: gcpConnectionService(connectAppConnectionById), + databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) }; }; diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index dfe2d1c64..f0640eea4 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -4,6 +4,12 @@ import { TAwsConnectionInput, TValidateAwsConnectionCredentials } from "@app/services/app-connection/aws"; +import { + TDatabricksConnection, + TDatabricksConnectionConfig, + TDatabricksConnectionInput, + TValidateDatabricksConnectionCredentials +} from "@app/services/app-connection/databricks"; import { TGitHubConnection, TGitHubConnectionConfig, @@ -11,11 +17,37 @@ import { TValidateGitHubConnectionCredentials } from "@app/services/app-connection/github"; +import { + TAzureAppConfigurationConnection, + TAzureAppConfigurationConnectionConfig, + TAzureAppConfigurationConnectionInput, + TValidateAzureAppConfigurationConnectionCredentials +} from "./azure-app-configuration"; +import { + TAzureKeyVaultConnection, + TAzureKeyVaultConnectionConfig, + TAzureKeyVaultConnectionInput, + TValidateAzureKeyVaultConnectionCredentials +} from "./azure-key-vault"; import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp"; -export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection); +export type TAppConnection = { id: string } & ( + | TAwsConnection + | TGitHubConnection + | TGcpConnection + | TAzureKeyVaultConnection + | TAzureAppConfigurationConnection + | TDatabricksConnection +); -export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput); +export type TAppConnectionInput = { id: string } & ( + | TAwsConnectionInput + | TGitHubConnectionInput + | TGcpConnectionInput + | TAzureKeyVaultConnectionInput + | TAzureAppConfigurationConnectionInput + | TDatabricksConnectionInput +); export type TCreateAppConnectionDTO = Pick< TAppConnectionInput, @@ -26,9 +58,18 @@ export type TUpdateAppConnectionDTO = Partial { +export const getAwsConnectionListItem = () => { const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig(); return { diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts new file mode 100644 index 000000000..450cb9255 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts @@ -0,0 +1,3 @@ +export enum AzureAppConfigurationConnectionMethod { + OAuth = "oauth" +} diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts new file mode 100644 index 000000000..9ccfc72b6 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts @@ -0,0 +1,98 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { AppConnection } from "../app-connection-enums"; +import { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums"; +import { + ExchangeCodeAzureResponse, + TAzureAppConfigurationConnectionConfig +} from "./azure-app-configuration-connection-types"; + +export const getAzureAppConfigurationConnectionListItem = () => { + const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + + return { + name: "Azure App Configuration" as const, + app: AppConnection.AzureAppConfiguration as const, + methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth], + oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + }; +}; + +export const validateAzureAppConfigurationConnectionCredentials = async ( + config: TAzureAppConfigurationConnectionConfig +) => { + const { credentials: inputCredentials, method } = config; + + const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); + + if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + throw new InternalServerError({ + message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` + }); + } + + let tokenResp: AxiosResponse | null = null; + let tokenError: AxiosError | null = null; + + try { + tokenResp = await request.post( + IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"), + new URLSearchParams({ + grant_type: "authorization_code", + code: inputCredentials.code, + scope: `openid offline_access https://azconfig.io/.default`, + client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, + client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` + }) + ); + } catch (e: unknown) { + if (e instanceof AxiosError) { + tokenError = e; + } else { + throw new BadRequestError({ + message: `Unable to validate connection - verify credentials` + }); + } + } + + if (tokenError) { + if (tokenError instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to get access token: ${ + (tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error" + }` + }); + } else { + throw new InternalServerError({ + message: "Failed to get access token" + }); + } + } + + if (!tokenResp) { + throw new InternalServerError({ + message: `Failed to get access token: Token was empty with no error` + }); + } + + switch (method) { + case AzureAppConfigurationConnectionMethod.OAuth: + return { + tenantId: inputCredentials.tenantId, + accessToken: tokenResp.data.access_token, + refreshToken: tokenResp.data.refresh_token, + expiresAt: Date.now() + tokenResp.data.expires_in * 1000 + }; + default: + throw new InternalServerError({ + message: `Unhandled Azure connection method: ${method as AzureAppConfigurationConnectionMethod}` + }); + } +}; diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts new file mode 100644 index 000000000..183376acf --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums"; + +export const AzureAppConfigurationConnectionOAuthInputCredentialsSchema = z.object({ + code: z.string().trim().min(1, "OAuth code required"), + tenantId: z.string().trim().optional() +}); + +export const AzureAppConfigurationConnectionOAuthOutputCredentialsSchema = z.object({ + tenantId: z.string().optional(), + accessToken: z.string(), + refreshToken: z.string(), + expiresAt: z.number() +}); + +export const ValidateAzureAppConfigurationConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(AzureAppConfigurationConnectionMethod.OAuth) + .describe(AppConnections.CREATE(AppConnection.AzureAppConfiguration).method), + credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AzureAppConfiguration).credentials + ) + }) +]); + +export const CreateAzureAppConfigurationConnectionSchema = ValidateAzureAppConfigurationConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration) +); + +export const UpdateAzureAppConfigurationConnectionSchema = z + .object({ + credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.AzureAppConfiguration).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration)); + +const BaseAzureAppConfigurationConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.AzureAppConfiguration) +}); + +export const AzureAppConfigurationConnectionSchema = z.intersection( + BaseAzureAppConfigurationConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AzureAppConfigurationConnectionMethod.OAuth), + credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema + }) + ]) +); + +export const SanitizedAzureAppConfigurationConnectionSchema = z.discriminatedUnion("method", [ + BaseAzureAppConfigurationConnectionSchema.extend({ + method: z.literal(AzureAppConfigurationConnectionMethod.OAuth), + credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({ + tenantId: true + }) + }) +]); + +export const AzureAppConfigurationConnectionListItemSchema = z.object({ + name: z.literal("Azure App Configuration"), + app: z.literal(AppConnection.AzureAppConfiguration), + methods: z.nativeEnum(AzureAppConfigurationConnectionMethod).array(), + oauthClientId: z.string().optional() +}); diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts new file mode 100644 index 000000000..db59a1558 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts @@ -0,0 +1,41 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + AzureAppConfigurationConnectionOAuthOutputCredentialsSchema, + AzureAppConfigurationConnectionSchema, + CreateAzureAppConfigurationConnectionSchema, + ValidateAzureAppConfigurationConnectionCredentialsSchema +} from "./azure-app-configuration-connection-schemas"; + +export type TAzureAppConfigurationConnection = z.infer; + +export type TAzureAppConfigurationConnectionInput = z.infer & { + app: AppConnection.AzureAppConfiguration; +}; + +export type TValidateAzureAppConfigurationConnectionCredentials = + typeof ValidateAzureAppConfigurationConnectionCredentialsSchema; + +export type TAzureAppConfigurationConnectionConfig = DiscriminativePick< + TAzureAppConfigurationConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type ExchangeCodeAzureResponse = { + token_type: string; + scope: string; + expires_in: number; + ext_expires_in: number; + access_token: string; + refresh_token: string; + id_token: string; +}; + +export type TAzureAppConfigurationConnectionCredentials = z.infer< + typeof AzureAppConfigurationConnectionOAuthOutputCredentialsSchema +>; diff --git a/backend/src/services/app-connection/azure-app-configuration/index.ts b/backend/src/services/app-connection/azure-app-configuration/index.ts new file mode 100644 index 000000000..5fbe876f5 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-app-configuration-connection-enums"; +export * from "./azure-app-configuration-connection-fns"; +export * from "./azure-app-configuration-connection-schemas"; +export * from "./azure-app-configuration-connection-types"; diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts new file mode 100644 index 000000000..895e88298 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts @@ -0,0 +1,3 @@ +export enum AzureKeyVaultConnectionMethod { + OAuth = "oauth" +} diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts new file mode 100644 index 000000000..12b1b3f3b --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -0,0 +1,170 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { + decryptAppConnectionCredentials, + encryptAppConnectionCredentials, + getAppConnectionMethodName +} from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAppConnectionDALFactory } from "../app-connection-dal"; +import { AppConnection } from "../app-connection-enums"; +import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums"; +import { + ExchangeCodeAzureResponse, + TAzureKeyVaultConnectionConfig, + TAzureKeyVaultConnectionCredentials +} from "./azure-key-vault-connection-types"; + +export const getAzureConnectionAccessToken = async ( + connectionId: string, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const appCfg = getConfig(); + if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + throw new BadRequestError({ + message: `Azure environment variables have not been configured` + }); + } + + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) { + throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` }); + } + + const credentials = (await decryptAppConnectionCredentials({ + orgId: appConnection.orgId, + kmsService, + encryptedCredentials: appConnection.encryptedCredentials + })) as TAzureKeyVaultConnectionCredentials; + + const { data } = await request.post( + IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"), + new URLSearchParams({ + grant_type: "refresh_token", + scope: `openid offline_access`, + client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, + client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + refresh_token: credentials.refreshToken + }) + ); + + const accessExpiresAt = new Date(); + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in); + + const updatedCredentials = { + ...credentials, + accessToken: data.access_token, + expiresAt: accessExpiresAt.getTime(), + refreshToken: data.refresh_token + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId: appConnection.orgId, + kmsService + }); + + await appConnectionDAL.update( + { id: connectionId }, + { + encryptedCredentials + } + ); + + return { + accessToken: data.access_token + }; +}; + +export const getAzureKeyVaultConnectionListItem = () => { + const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + + return { + name: "Azure Key Vault" as const, + app: AppConnection.AzureKeyVault as const, + methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth], + oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + }; +}; + +export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => { + const { credentials: inputCredentials, method } = config; + + const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); + + if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + throw new InternalServerError({ + message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` + }); + } + + let tokenResp: AxiosResponse | null = null; + let tokenError: AxiosError | null = null; + + try { + tokenResp = await request.post( + IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"), + new URLSearchParams({ + grant_type: "authorization_code", + code: inputCredentials.code, + scope: `openid offline_access https://vault.azure.net/.default`, + client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, + client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` + }) + ); + } catch (e: unknown) { + if (e instanceof AxiosError) { + tokenError = e; + } else { + throw new BadRequestError({ + message: `Unable to validate connection - verify credentials` + }); + } + } + + if (tokenError) { + if (tokenError instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to get access token: ${ + (tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error" + }` + }); + } else { + throw new InternalServerError({ + message: "Failed to get access token" + }); + } + } + + if (!tokenResp) { + throw new InternalServerError({ + message: `Failed to get access token: Token was empty with no error` + }); + } + + switch (method) { + case AzureKeyVaultConnectionMethod.OAuth: + return { + tenantId: inputCredentials.tenantId, + accessToken: tokenResp.data.access_token, + refreshToken: tokenResp.data.refresh_token, + expiresAt: Date.now() + tokenResp.data.expires_in * 1000 + }; + default: + throw new InternalServerError({ + message: `Unhandled Azure connection method: ${method as AzureKeyVaultConnectionMethod}` + }); + } +}; diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts new file mode 100644 index 000000000..f3c7c43b8 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums"; + +export const AzureKeyVaultConnectionOAuthInputCredentialsSchema = z.object({ + code: z.string().trim().min(1, "OAuth code required"), + tenantId: z.string().trim().optional() +}); + +export const AzureKeyVaultConnectionOAuthOutputCredentialsSchema = z.object({ + tenantId: z.string().optional(), + accessToken: z.string(), + refreshToken: z.string(), + expiresAt: z.number() +}); + +export const ValidateAzureKeyVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(AzureKeyVaultConnectionMethod.OAuth) + .describe(AppConnections.CREATE(AppConnection.AzureKeyVault).method), + credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AzureKeyVault).credentials + ) + }) +]); + +export const CreateAzureKeyVaultConnectionSchema = ValidateAzureKeyVaultConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.AzureKeyVault) +); + +export const UpdateAzureKeyVaultConnectionSchema = z + .object({ + credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.AzureKeyVault).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureKeyVault)); + +const BaseAzureKeyVaultConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.AzureKeyVault) +}); + +export const AzureKeyVaultConnectionSchema = z.intersection( + BaseAzureKeyVaultConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AzureKeyVaultConnectionMethod.OAuth), + credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema + }) + ]) +); + +export const SanitizedAzureKeyVaultConnectionSchema = z.discriminatedUnion("method", [ + BaseAzureKeyVaultConnectionSchema.extend({ + method: z.literal(AzureKeyVaultConnectionMethod.OAuth), + credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({ + tenantId: true + }) + }) +]); + +export const AzureKeyVaultConnectionListItemSchema = z.object({ + name: z.literal("Azure Key Vault"), + app: z.literal(AppConnection.AzureKeyVault), + methods: z.nativeEnum(AzureKeyVaultConnectionMethod).array(), + oauthClientId: z.string().optional() +}); diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts new file mode 100644 index 000000000..d784ca100 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts @@ -0,0 +1,38 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + AzureKeyVaultConnectionOAuthOutputCredentialsSchema, + AzureKeyVaultConnectionSchema, + CreateAzureKeyVaultConnectionSchema, + ValidateAzureKeyVaultConnectionCredentialsSchema +} from "./azure-key-vault-connection-schemas"; + +export type TAzureKeyVaultConnection = z.infer; + +export type TAzureKeyVaultConnectionInput = z.infer & { + app: AppConnection.AzureKeyVault; +}; + +export type TValidateAzureKeyVaultConnectionCredentials = typeof ValidateAzureKeyVaultConnectionCredentialsSchema; + +export type TAzureKeyVaultConnectionConfig = DiscriminativePick< + TAzureKeyVaultConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type ExchangeCodeAzureResponse = { + token_type: string; + scope: string; + expires_in: number; + ext_expires_in: number; + access_token: string; + refresh_token: string; + id_token: string; +}; + +export type TAzureKeyVaultConnectionCredentials = z.infer; diff --git a/backend/src/services/app-connection/azure-key-vault/index.ts b/backend/src/services/app-connection/azure-key-vault/index.ts new file mode 100644 index 000000000..b80b07c17 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-key-vault-connection-enums"; +export * from "./azure-key-vault-connection-fns"; +export * from "./azure-key-vault-connection-schemas"; +export * from "./azure-key-vault-connection-types"; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-enums.ts b/backend/src/services/app-connection/databricks/databricks-connection-enums.ts new file mode 100644 index 000000000..b65161e45 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-enums.ts @@ -0,0 +1,3 @@ +export enum DatabricksConnectionMethod { + ServicePrincipal = "service-principal" +} diff --git a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts new file mode 100644 index 000000000..a12fe290c --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts @@ -0,0 +1,92 @@ +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { DatabricksConnectionMethod } from "./databricks-connection-enums"; +import { + TAuthorizeDatabricksConnection, + TDatabricksConnection, + TDatabricksConnectionConfig +} from "./databricks-connection-types"; + +export const getDatabricksConnectionListItem = () => { + return { + name: "Databricks" as const, + app: AppConnection.Databricks as const, + methods: Object.values(DatabricksConnectionMethod) as [DatabricksConnectionMethod.ServicePrincipal] + }; +}; + +const authorizeDatabricksConnection = async ({ + clientId, + clientSecret, + workspaceUrl +}: Pick) => { + const { data } = await request.post( + `${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`, + "grant_type=client_credentials&scope=all-apis", + { + auth: { + username: clientId, + password: clientSecret + }, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + } + ); + + return { accessToken: data.access_token, expiresAt: data.expires_in * 1000 + Date.now() }; +}; + +export const getDatabricksConnectionAccessToken = async ( + { id, orgId, credentials }: TDatabricksConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { clientSecret, clientId, workspaceUrl, accessToken, expiresAt } = credentials; + + // get new token if less than 10 minutes from expiry + if (Date.now() < expiresAt - 10_000) { + return accessToken; + } + + const authData = await authorizeDatabricksConnection({ clientId, clientSecret, workspaceUrl }); + + const updatedCredentials: TDatabricksConnection["credentials"] = { + ...credentials, + ...authData + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(id, { encryptedCredentials }); + + return authData.accessToken; +}; + +export const validateDatabricksConnectionCredentials = async (appConnection: TDatabricksConnectionConfig) => { + const { credentials } = appConnection; + + try { + const { accessToken, expiresAt } = await authorizeDatabricksConnection(appConnection.credentials); + + return { + ...credentials, + accessToken, + expiresAt + }; + } catch (e: unknown) { + throw new BadRequestError({ + message: `Unable to validate connection - verify credentials` + }); + } +}; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts b/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts new file mode 100644 index 000000000..af1a75127 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { DatabricksConnectionMethod } from "./databricks-connection-enums"; + +export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.object({ + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client Secret required"), + workspaceUrl: z.string().trim().url().min(1, "Workspace URL required") +}); + +export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z + .object({ + accessToken: z.string(), + expiresAt: z.number() + }) + .merge(DatabricksConnectionServicePrincipalInputCredentialsSchema); + +const BaseDatabricksConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Databricks) }); + +export const DatabricksConnectionSchema = z.intersection( + BaseDatabricksConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(DatabricksConnectionMethod.ServicePrincipal), + credentials: DatabricksConnectionServicePrincipalOutputCredentialsSchema + }) + ]) +); + +export const SanitizedDatabricksConnectionSchema = z.discriminatedUnion("method", [ + BaseDatabricksConnectionSchema.extend({ + method: z.literal(DatabricksConnectionMethod.ServicePrincipal), + credentials: DatabricksConnectionServicePrincipalOutputCredentialsSchema.pick({ + clientId: true, + workspaceUrl: true + }) + }) +]); + +export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(DatabricksConnectionMethod.ServicePrincipal) + .describe(AppConnections?.CREATE(AppConnection.Databricks).method), + credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Databricks).credentials + ) + }) +]); + +export const CreateDatabricksConnectionSchema = ValidateDatabricksConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Databricks) +); + +export const UpdateDatabricksConnectionSchema = z + .object({ + credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Databricks).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Databricks)); + +export const DatabricksConnectionListItemSchema = z.object({ + name: z.literal("Databricks"), + app: z.literal(AppConnection.Databricks), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), + methods: z.nativeEnum(DatabricksConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/databricks/databricks-connection-service.ts b/backend/src/services/app-connection/databricks/databricks-connection-service.ts new file mode 100644 index 000000000..37b88705a --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-service.ts @@ -0,0 +1,60 @@ +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { getDatabricksConnectionAccessToken } from "@app/services/app-connection/databricks/databricks-connection-fns"; +import { + TDatabricksConnection, + TDatabricksListSecretScopesResponse +} from "@app/services/app-connection/databricks/databricks-connection-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listDatabricksSecretScopes = async ( + appConnection: TDatabricksConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { + credentials: { workspaceUrl } + } = appConnection; + + const accessToken = await getDatabricksConnectionAccessToken(appConnection, appConnectionDAL, kmsService); + + const { data } = await request.get( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/scopes/list`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // not present in response if no scopes exists + return data.scopes ?? []; +}; + +export const databricksConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listSecretScopes = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Databricks, connectionId, actor); + + const secretScopes = await listDatabricksSecretScopes(appConnection, appConnectionDAL, kmsService); + + return secretScopes; + }; + + return { + listSecretScopes + }; +}; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-types.ts b/backend/src/services/app-connection/databricks/databricks-connection-types.ts new file mode 100644 index 000000000..e45610b16 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-types.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + CreateDatabricksConnectionSchema, + DatabricksConnectionSchema, + ValidateDatabricksConnectionCredentialsSchema +} from "./databricks-connection-schemas"; + +export type TDatabricksConnection = z.infer; + +export type TDatabricksConnectionInput = z.infer & { + app: AppConnection.Databricks; +}; + +export type TValidateDatabricksConnectionCredentials = typeof ValidateDatabricksConnectionCredentialsSchema; + +export type TDatabricksConnectionConfig = DiscriminativePick< + TDatabricksConnection, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TAuthorizeDatabricksConnection = { + access_token: string; + scope: string; + token_type: string; + expires_in: number; +}; + +export type TDatabricksListSecretScopesResponse = { + scopes?: { name: string; backend_type: string; keyvault_metadata: { resource_id: string; dns_name: string } }[]; +}; diff --git a/backend/src/services/app-connection/databricks/index.ts b/backend/src/services/app-connection/databricks/index.ts new file mode 100644 index 000000000..844000af4 --- /dev/null +++ b/backend/src/services/app-connection/databricks/index.ts @@ -0,0 +1,4 @@ +export * from "./databricks-connection-enums"; +export * from "./databricks-connection-fns"; +export * from "./databricks-connection-schemas"; +export * from "./databricks-connection-types"; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts index 3c4abb6d9..8f54735e4 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts @@ -17,7 +17,7 @@ import { TGcpConnectionConfig } from "./gcp-connection-types"; -export const getGcpAppConnectionListItem = () => { +export const getGcpConnectionListItem = () => { return { name: "GCP" as const, app: AppConnection.GCP as const, diff --git a/backend/src/services/cmek/cmek-service.ts b/backend/src/services/cmek/cmek-service.ts index 775367e6e..5e74a5bac 100644 --- a/backend/src/services/cmek/cmek-service.ts +++ b/backend/src/services/cmek/cmek-service.ts @@ -3,7 +3,8 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { TCmekDecryptDTO, @@ -44,17 +45,31 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek); - const cmek = await kmsService.generateKmsKey({ - ...dto, - projectId, - isReserved: false - }); + try { + const cmek = await kmsService.generateKmsKey({ + ...dto, + projectId, + isReserved: false + }); - return cmek; + return { + ...cmek, + version: 1, + encryptionAlgorithm: dto.encryptionAlgorithm + }; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `A KMS key with the name "${dto.name}" already exists for the project with ID "${projectId}"` + }); + } + + throw err; + } }; const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: OrgServiceActor) => { - const key = await kmsDAL.findById(keyId); + const key = await kmsDAL.findCmekById(keyId); if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); @@ -71,13 +86,27 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek); - const cmek = await kmsDAL.updateById(keyId, data); + try { + const cmek = await kmsDAL.updateById(keyId, data); - return cmek; + return { + ...cmek, + version: key.version, + encryptionAlgorithm: key.encryptionAlgorithm + }; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `A KMS key with the name "${data.name!}" already exists for the project with ID "${key.projectId}"` + }); + } + + throw err; + } }; const deleteCmekById = async (keyId: string, actor: OrgServiceActor) => { - const key = await kmsDAL.findById(keyId); + const key = await kmsDAL.findCmekById(keyId); if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); @@ -94,9 +123,9 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek); - const cmek = kmsDAL.deleteById(keyId); + await kmsDAL.deleteById(keyId); - return cmek; + return key; }; const listCmeksByProjectId = async ( @@ -120,15 +149,58 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); - const { keys: cmeks, totalCount } = await kmsDAL.findKmsKeysByProjectId({ projectId, ...filters }); + const { keys: cmeks, totalCount } = await kmsDAL.listCmeksByProjectId({ projectId, ...filters }); return { cmeks, totalCount }; }; + const findCmekById = async (keyId: string, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + return key; + }; + + const findCmekByName = async (keyName: string, projectId: string, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekByName(keyName, projectId); + + if (!key) + throw new NotFoundError({ message: `Key with name "${keyName}" not found for project with ID "${projectId}"` }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + return key; + }; + const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: OrgServiceActor) => { const key = await kmsDAL.findById(keyId); - if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); @@ -155,7 +227,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => { const key = await kmsDAL.findById(keyId); - if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); @@ -185,6 +257,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj deleteCmekById, listCmeksByProjectId, cmekEncrypt, - cmekDecrypt + cmekDecrypt, + findCmekById, + findCmekByName }; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts index 2cb7b4ea4..2cc736f1e 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts @@ -1,7 +1,7 @@ import { z } from "zod"; const twelveDigitRegex = /^\d{12}$/; -const arnRegex = /^arn:aws:iam::\d{12}:(user\/[\w-]+|role\/[\w-]+|\*)$/; +const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/; export const validateAccountIds = z .string() diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 4508a255d..a5677894d 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -3,28 +3,21 @@ import axios, { AxiosError } from "axios"; import https from "https"; import jwt from "jsonwebtoken"; -import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal"; import { extractK8sUsername } from "./identity-kubernetes-auth-fns"; import { @@ -43,9 +36,9 @@ type TIdentityKubernetesAuthServiceFactoryDep = { >; identityAccessTokenDAL: Pick; identityOrgMembershipDAL: Pick; - orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; + kmsService: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -54,9 +47,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, - orgBotDAL, permissionService, - licenseService + licenseService, + kmsService }: TIdentityKubernetesAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); @@ -75,42 +68,21 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } = - identityKubernetesAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); } let tokenReviewerJwt = ""; - if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) { - tokenReviewerJwt = decryptSymmetric({ - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { + tokenReviewerJwt = decryptor({ + cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString(); } const { data } = await axios @@ -297,79 +269,25 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: identityMembershipOrg.orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const { - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag - } = encryptSymmetric(tokenReviewerJwt, key); - const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const doc = await identityKubernetesAuthDAL.create( { identityId: identityMembershipOrg.identityId, kubernetesHost, - encryptedCaCert, - caCertIV, - caCertTag, - encryptedTokenReviewerJwt, - tokenReviewerJwtIV, - tokenReviewerJwtTag, allowedNamespaces, allowedNames, allowedAudience, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), + encryptedKubernetesTokenReviewerJwt: encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob, + encryptedKubernetesCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob }, tx ); @@ -455,61 +373,34 @@ export const identityKubernetesAuthServiceFactory = ({ : undefined }; - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCaCert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } if (tokenReviewerJwt !== undefined) { - const { - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag - } = encryptSymmetric(tokenReviewerJwt, key); - updateQuery.encryptedTokenReviewerJwt = encryptedTokenReviewerJwt; - updateQuery.tokenReviewerJwtIV = tokenReviewerJwtIV; - updateQuery.tokenReviewerJwtTag = tokenReviewerJwtTag; + updateQuery.encryptedKubernetesTokenReviewerJwt = encryptor({ + plainText: Buffer.from(tokenReviewerJwt) + }).cipherTextBlob; } const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery); - const updatedCACert = - updatedKubernetesAuth.encryptedCaCert && updatedKubernetesAuth.caCertIV && updatedKubernetesAuth.caCertTag - ? decryptSymmetric({ - ciphertext: updatedKubernetesAuth.encryptedCaCert, - iv: updatedKubernetesAuth.caCertIV, - tag: updatedKubernetesAuth.caCertTag, - key - }) - : ""; + const updatedCACert = updatedKubernetesAuth.encryptedKubernetesCaCertificate + ? decryptor({ + cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesCaCertificate + }).toString() + : ""; - const updatedTokenReviewerJwt = - updatedKubernetesAuth.encryptedTokenReviewerJwt && - updatedKubernetesAuth.tokenReviewerJwtIV && - updatedKubernetesAuth.tokenReviewerJwtTag - ? decryptSymmetric({ - ciphertext: updatedKubernetesAuth.encryptedTokenReviewerJwt, - iv: updatedKubernetesAuth.tokenReviewerJwtIV, - tag: updatedKubernetesAuth.tokenReviewerJwtTag, - key - }) - : ""; + const updatedTokenReviewerJwt = updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt + ? decryptor({ + cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString() + : ""; return { ...updatedKubernetesAuth, @@ -545,41 +436,21 @@ export const identityKubernetesAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } = - identityKubernetesAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); } let tokenReviewerJwt = ""; - if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) { - tokenReviewerJwt = decryptSymmetric({ - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { + tokenReviewerJwt = decryptor({ + cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString(); } return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId }; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index a1dbed46b..ff7256a9c 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -4,20 +4,12 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { generateAsymmetricKeyPair } from "@app/lib/crypto"; -import { - decryptSymmetric, - encryptSymmetric, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -25,7 +17,8 @@ import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; -import { TOrgBotDALFactory } from "../org/org-bot-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; import { @@ -42,7 +35,7 @@ type TIdentityOidcAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; - orgBotDAL: Pick; + kmsService: Pick; }; export type TIdentityOidcAuthServiceFactory = ReturnType; @@ -53,7 +46,7 @@ export const identityOidcAuthServiceFactory = ({ permissionService, licenseService, identityAccessTokenDAL, - orgBotDAL + kmsService }: TIdentityOidcAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -70,31 +63,14 @@ export const identityOidcAuthServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${identityMembershipOrg.orgId}'`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag } = identityOidcAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityOidcAuth.encryptedCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString(); } const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert }); @@ -264,64 +240,17 @@ export const identityOidcAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: identityMembershipOrg.orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const doc = await identityOidcAuthDAL.create( { identityId: identityMembershipOrg.identityId, oidcDiscoveryUrl, - encryptedCaCert, - caCertIV, - caCertTag, + encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, boundIssuer, boundAudiences, boundClaims, @@ -415,38 +344,19 @@ export const identityOidcAuthServiceFactory = ({ : undefined }; - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${identityMembershipOrg.orgId}'`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCaCert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } const updatedOidcAuth = await identityOidcAuthDAL.updateById(identityOidcAuth.id, updateQuery); - const updatedCACert = - updatedOidcAuth.encryptedCaCert && updatedOidcAuth.caCertIV && updatedOidcAuth.caCertTag - ? decryptSymmetric({ - ciphertext: updatedOidcAuth.encryptedCaCert, - iv: updatedOidcAuth.caCertIV, - tag: updatedOidcAuth.caCertTag, - key - }) - : ""; + const updatedCACert = updatedOidcAuth.encryptedCaCertificate + ? decryptor({ cipherTextBlob: updatedOidcAuth.encryptedCaCertificate }).toString() + : ""; return { ...updatedOidcAuth, @@ -476,27 +386,14 @@ export const identityOidcAuthServiceFactory = ({ const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const caCert = decryptSymmetric({ - ciphertext: identityOidcAuth.encryptedCaCert, - iv: identityOidcAuth.caCertIV, - tag: identityOidcAuth.caCertTag, - key - }); + const caCert = identityOidcAuth.encryptedCaCertificate + ? decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString() + : ""; return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; }; diff --git a/backend/src/services/kms/kms-key-dal.ts b/backend/src/services/kms/kms-key-dal.ts index 3b21d1e3e..c5c8a5ec5 100644 --- a/backend/src/services/kms/kms-key-dal.ts +++ b/backend/src/services/kms/kms-key-dal.ts @@ -3,12 +3,32 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { KmsKeysSchema, TableName, TInternalKms, TKmsKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { CmekOrderBy, TListCmeksByProjectIdDTO } from "@app/services/cmek/cmek-types"; export type TKmsKeyDALFactory = ReturnType; +type TCmekFindFilter = Parameters>[0]; + +const baseCmekQuery = ({ filter, db, tx }: { db: TDbClient; filter?: TCmekFindFilter; tx?: Knex }) => { + const query = (tx || db.replicaNode())(TableName.KmsKey) + .where(`${TableName.KmsKey}.isReserved`, false) + .join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`) + .select( + selectAllTableCols(TableName.KmsKey), + db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms), + db.ref("version").withSchema(TableName.InternalKms) + ); + + if (filter) { + /* eslint-disable @typescript-eslint/no-misused-promises */ + void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.KmsKey, filter))); + } + + return query; +}; + export const kmskeyDALFactory = (db: TDbClient) => { const kmsOrm = ormify(db, TableName.KmsKey); @@ -101,7 +121,7 @@ export const kmskeyDALFactory = (db: TDbClient) => { } }; - const findKmsKeysByProjectId = async ( + const listCmeksByProjectId = async ( { projectId, offset = 0, @@ -120,6 +140,7 @@ export const kmskeyDALFactory = (db: TDbClient) => { void qb.whereILike("name", `%${search}%`); } }) + .where(`${TableName.KmsKey}.isReserved`, false) .join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`) .select< (TKmsKeys & @@ -146,5 +167,33 @@ export const kmskeyDALFactory = (db: TDbClient) => { } }; - return { ...kmsOrm, findByIdWithAssociatedKms, findKmsKeysByProjectId, findProjectCmeks }; + const findCmekById = async (id: string, tx?: Knex) => { + try { + const key = await baseCmekQuery({ + filter: { id }, + db, + tx + }).first(); + + return key; + } catch (error) { + throw new DatabaseError({ error, name: "Find by ID - KMS Key" }); + } + }; + + const findCmekByName = async (keyName: string, projectId: string, tx?: Knex) => { + try { + const key = await baseCmekQuery({ + filter: { name: keyName, projectId }, + db, + tx + }).first(); + + return key; + } catch (error) { + throw new DatabaseError({ error, name: "Find by Name - KMS Key" }); + } + }; + + return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName, findProjectCmeks }; }; diff --git a/backend/src/services/kms/kms-root-config-dal.ts b/backend/src/services/kms/kms-root-config-dal.ts index f448e2df8..8745d286e 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -1,10 +1,24 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; +import { Knex } from "knex"; export type TKmsRootConfigDALFactory = ReturnType; export const kmsRootConfigDALFactory = (db: TDbClient) => { const kmsOrm = ormify(db, TableName.KmsServerRootConfig); - return kmsOrm; + + const findById = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.KmsServerRootConfig) + .where({ id } as never) + .first("*"); + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Find by id" }); + } + }; + + return { ...kmsOrm, findById }; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 9cc960c8c..cfd64a89a 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -12,8 +12,8 @@ import { TExternalKmsProviderFns } from "@app/ee/services/external-kms/providers/model"; import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; -import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; +import { TEnvConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; import { generateHash } from "@app/lib/crypto/encryption"; @@ -46,23 +46,22 @@ type TKmsServiceFactoryDep = { kmsDAL: TKmsKeyDALFactory; projectDAL: Pick; orgDAL: Pick; - kmsRootConfigDAL: Pick; + kmsRootConfigDAL: Pick; keyStore: Pick; internalKmsDAL: Pick; hsmService: THsmServiceFactory; + envConfig: Pick; }; export type TKmsServiceFactory = ReturnType; -const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key"; -const KMS_ROOT_CREATION_WAIT_TIME = 10; - // akhilmhdh: Don't edit this value. This is measured for blob concatination in kms const KMS_VERSION = "v01"; const KMS_VERSION_BLOB_LENGTH = 3; const KmsSanitizedSchema = KmsKeysSchema.extend({ isExternal: z.boolean() }); export const kmsServiceFactory = ({ + envConfig, kmsDAL, kmsRootConfigDAL, keyStore, @@ -541,7 +540,8 @@ export const kmsServiceFactory = ({ } const kmsDecryptor = await decryptWithKmsKey({ - kmsId: kmsKeyId + kmsId: kmsKeyId, + tx: trx }); return kmsDecryptor({ @@ -703,10 +703,8 @@ export const kmsServiceFactory = ({ }; const $getBasicEncryptionKey = () => { - const appCfg = getConfig(); - - const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; - const isBase64 = !appCfg.ENCRYPTION_KEY; + const encryptionKey = envConfig.ENCRYPTION_KEY || envConfig.ROOT_ENCRYPTION_KEY; + const isBase64 = !envConfig.ENCRYPTION_KEY; if (!encryptionKey) throw new Error( "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" @@ -942,54 +940,33 @@ export const kmsServiceFactory = ({ return { id, name, orgId, isExternal }; }; - // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { - const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null); - if (!lock) { - await keyStore.waitTillReady({ - key: KMS_ROOT_CREATION_WAIT_KEY, - keyCheckCb: (val) => val === "true", - waitingCb: () => logger.info("KMS. Waiting for leader to finish creation of KMS Root Key") + const kmsRootConfig = await kmsRootConfigDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.KmsRootKeyInit]); + // check if KMS root key was already generated and saved in DB + const existingRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + if (existingRootConfig) return existingRootConfig; + + logger.info("KMS: Generating new ROOT Key"); + const newRootKey = randomSecureBytes(32); + const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => { + logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key"); + throw err; }); - } - // check if KMS root key was already generated and saved in DB - const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); - - // case 1: a root key already exists in the DB - if (kmsRootConfig) { - if (lock) await lock.release(); - logger.info(`KMS: Encrypted ROOT Key found from DB. Decrypting. [strategy=${kmsRootConfig.encryptionStrategy}]`); - - const decryptedRootKey = await $decryptRootKey(kmsRootConfig); - - // set the flag so that other instance nodes can start - await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true"); - logger.info("KMS: Loading ROOT Key into Memory."); - ROOT_ENCRYPTION_KEY = decryptedRootKey; - return; - } - - // case 2: no config is found, so we create a new root key with basic encryption - logger.info("KMS: Generating new ROOT Key"); - const newRootKey = randomSecureBytes(32); - const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => { - logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key"); - throw err; + const newRootConfig = await kmsRootConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: KMS_ROOT_CONFIG_UUID, + encryptedRootKey, + encryptionStrategy: RootKeyEncryptionStrategy.Software + }); + return newRootConfig; }); - await kmsRootConfigDAL.create({ - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - id: KMS_ROOT_CONFIG_UUID, - encryptedRootKey, - encryptionStrategy: RootKeyEncryptionStrategy.Software - }); + const decryptedRootKey = await $decryptRootKey(kmsRootConfig); - // set the flag so that other instance nodes can start - await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true"); - logger.info("KMS: Saved and loaded ROOT Key into memory"); - if (lock) await lock.release(); - ROOT_ENCRYPTION_KEY = newRootKey; + logger.info("KMS: Loading ROOT Key into Memory."); + ROOT_ENCRYPTION_KEY = decryptedRootKey; }; const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => { diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 09bc6460d..1d695a5ff 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -9,7 +9,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorAuthMethod } from "../auth/auth-type"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 2c6b8e2da..83a59b6af 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -4,8 +4,17 @@ import { ProjectType, TProjectKeys } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; -import { CaStatus } from "../certificate-authority/certificate-authority-types"; -import { KmsType } from "../kms/kms-types"; + +enum KmsType { + External = "external", + Internal = "internal" +} + +enum CaStatus { + ACTIVE = "active", + DISABLED = "disabled", + PENDING_CERTIFICATE = "pending-certificate" +} export enum ProjectFilterType { ID = "id", diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts index a495f9e83..6d7739afa 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -69,6 +69,8 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise; +type TAwsSecretValuesRecord = Record; + +const MAX_RETRIES = 5; +const BATCH_SIZE = 20; + +const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => { + const { destinationConfig, connection } = secretSync; + + const config = await getAwsConnectionConfig(connection, destinationConfig.region); + + const secretsManagerClient = new SecretsManagerClient({ + region: config.region, + credentials: config.credentials! + }); + + return secretsManagerClient; +}; + +const sleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + +const getSecretsRecord = async (client: SecretsManagerClient): Promise => { + const awsSecretsRecord: TAwsSecretsRecord = {}; + let hasNext = true; + let nextToken: string | undefined; + let attempt = 0; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const output = await client.send(new ListSecretsCommand({ NextToken: nextToken })); + + attempt = 0; + + if (output.SecretList) { + output.SecretList.forEach((secretEntry) => { + if (secretEntry.Name) { + awsSecretsRecord[secretEntry.Name] = secretEntry; + } + }); + } + + hasNext = Boolean(output.NextToken); + nextToken = output.NextToken; + } catch (e) { + if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + attempt += 1; + // eslint-disable-next-line no-await-in-loop + await sleep(); + // eslint-disable-next-line no-continue + continue; + } + + throw e; + } + } + + return awsSecretsRecord; +}; + +const getSecretValuesRecord = async ( + client: SecretsManagerClient, + awsSecretsRecord: TAwsSecretsRecord +): Promise => { + const awsSecretValuesRecord: TAwsSecretValuesRecord = {}; + let attempt = 0; + + const secretIdList = Object.keys(awsSecretsRecord); + + for (let i = 0; i < secretIdList.length; i += BATCH_SIZE) { + const batchSecretIds = secretIdList.slice(i, i + BATCH_SIZE); + let hasNext = true; + let nextToken: string | undefined; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const output = await client.send( + new BatchGetSecretValueCommand({ + SecretIdList: batchSecretIds, + NextToken: nextToken + }) + ); + + attempt = 0; + + if (output.SecretValues) { + output.SecretValues.forEach((secretValueEntry) => { + if (secretValueEntry.Name) { + awsSecretValuesRecord[secretValueEntry.Name] = secretValueEntry; + } + }); + } + + hasNext = Boolean(output.NextToken); + nextToken = output.NextToken; + } catch (e) { + if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + attempt += 1; + // eslint-disable-next-line no-await-in-loop + await sleep(); + // eslint-disable-next-line no-continue + continue; + } + + throw e; + } + } + } + + return awsSecretValuesRecord; +}; + +const createSecret = async ( + client: SecretsManagerClient, + input: CreateSecretCommandInput, + attempt = 0 +): Promise => { + try { + return await client.send(new CreateSecretCommand(input)); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return createSecret(client, input, attempt + 1); + } + throw error; + } +}; + +const updateSecret = async ( + client: SecretsManagerClient, + input: UpdateSecretCommandInput, + attempt = 0 +): Promise => { + try { + return await client.send(new UpdateSecretCommand(input)); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return updateSecret(client, input, attempt + 1); + } + throw error; + } +}; + +const deleteSecret = async ( + client: SecretsManagerClient, + secretKey: string, + attempt = 0 +): Promise => { + try { + return await client.send(new DeleteSecretCommand({ SecretId: secretKey, ForceDeleteWithoutRecovery: true })); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return deleteSecret(client, secretKey, attempt + 1); + } + throw error; + } +}; + +export const AwsSecretsManagerSyncFns = { + syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig } = secretSync; + + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + + const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + // skip secrets that don't have a value set + if (!value) { + // eslint-disable-next-line no-continue + continue; + } + + if (awsSecretsRecord[key]) { + // skip secrets that haven't changed + if (awsValuesRecord[key]?.SecretString === value) { + // eslint-disable-next-line no-continue + continue; + } + + try { + await updateSecret(client, { + SecretId: key, + SecretString: value + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } else { + try { + await createSecret(client, { + Name: key, + SecretString: value + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + + for await (const secretKey of Object.keys(awsSecretsRecord)) { + if (!(secretKey in secretMap) || !secretMap[secretKey].value) { + try { + await deleteSecret(client, secretKey); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey + }); + } + } + } + } else { + // Many-To-One Mapping + + const secretValue = JSON.stringify( + Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value])) + ); + + if (awsValuesRecord[destinationConfig.secretName]) { + await updateSecret(client, { + SecretId: destinationConfig.secretName, + SecretString: secretValue + }); + } else { + await createSecret(client, { + Name: destinationConfig.secretName, + SecretString: secretValue + }); + } + + for await (const secretKey of Object.keys(awsSecretsRecord)) { + if (secretKey === destinationConfig.secretName) { + // eslint-disable-next-line no-continue + continue; + } + + try { + await deleteSecret(client, secretKey); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey + }); + } + } + } + }, + getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise => { + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); + + const { destinationConfig } = secretSync; + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + return Object.fromEntries( + Object.keys(awsSecretsRecord).map((key) => [key, { value: awsValuesRecord[key].SecretString ?? "" }]) + ); + } + + // Many-To-One Mapping + + const secretValueEntry = awsValuesRecord[destinationConfig.secretName]; + + if (!secretValueEntry) return {}; + + try { + const parsedValue = (secretValueEntry.SecretString ? JSON.parse(secretValueEntry.SecretString) : {}) as Record< + string, + string + >; + + return Object.fromEntries(Object.entries(parsedValue).map(([key, value]) => [key, { value }])); + } catch { + throw new SecretSyncError({ + message: + "Failed to import secrets. Invalid format for Many-To-One mapping behavior: requires key/value configuration.", + shouldRetry: false + }); + } + }, + removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig } = secretSync; + + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + for await (const secretKey of Object.keys(awsSecretsRecord)) { + if (secretKey in secretMap) { + try { + await deleteSecret(client, secretKey); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey + }); + } + } + } + } else { + await deleteSecret(client, destinationConfig.secretName); + } + } +}; diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts new file mode 100644 index 000000000..6de014502 --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; + +const AwsSecretsManagerSyncDestinationConfigSchema = z + .discriminatedUnion("mappingBehavior", [ + z.object({ + mappingBehavior: z + .literal(AwsSecretsManagerSyncMappingBehavior.OneToOne) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior) + }), + z.object({ + mappingBehavior: z + .literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior), + secretName: z + .string() + .regex( + /^[a-zA-Z0-9/_+=.@-]+$/, + "Secret name must contain only alphanumeric characters and the characters /_+=.@-" + ) + .min(1, "Secret name is required") + .max(256, "Secret name cannot exceed 256 characters") + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName) + }) + ]) + .and( + z.object({ + region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.region) + }) + ); + +export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({ + destination: z.literal(SecretSync.AWSSecretsManager), + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema +}); + +export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AWSSecretsManager +).extend({ + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema +}); + +export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AWSSecretsManager +).extend({ + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional() +}); + +export const AwsSecretsManagerSyncListItemSchema = z.object({ + name: z.literal("AWS Secrets Manager"), + connection: z.literal(AppConnection.AWS), + destination: z.literal(SecretSync.AWSSecretsManager), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts new file mode 100644 index 000000000..848dce6bc --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TAwsConnection } from "@app/services/app-connection/aws"; + +import { + AwsSecretsManagerSyncListItemSchema, + AwsSecretsManagerSyncSchema, + CreateAwsSecretsManagerSyncSchema +} from "./aws-secrets-manager-sync-schemas"; + +export type TAwsSecretsManagerSync = z.infer; + +export type TAwsSecretsManagerSyncInput = z.infer; + +export type TAwsSecretsManagerSyncListItem = z.infer; + +export type TAwsSecretsManagerSyncWithCredentials = TAwsSecretsManagerSync & { + connection: TAwsConnection; +}; diff --git a/backend/src/services/secret-sync/aws-secrets-manager/index.ts b/backend/src/services/secret-sync/aws-secrets-manager/index.ts new file mode 100644 index 000000000..ff6ccff98 --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/index.ts @@ -0,0 +1,4 @@ +export * from "./aws-secrets-manager-sync-constants"; +export * from "./aws-secrets-manager-sync-fns"; +export * from "./aws-secrets-manager-sync-schemas"; +export * from "./aws-secrets-manager-sync-types"; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts new file mode 100644 index 000000000..07876f088 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Azure App Configuration", + destination: SecretSync.AzureAppConfiguration, + connection: AppConnection.AzureAppConfiguration, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts new file mode 100644 index 000000000..8c0587599 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -0,0 +1,214 @@ +/* eslint-disable no-await-in-loop */ +import https from "https"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; +import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types"; + +type TAzureAppConfigurationSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +interface AzureAppConfigKeyValue { + key: string; + value: string; + label?: string; +} + +export const azureAppConfigurationSyncFactory = ({ + kmsService, + appConnectionDAL +}: TAzureAppConfigurationSyncFactoryDeps) => { + const $getCompleteAzureAppConfigValues = async (accessToken: string, baseURL: string, url: string) => { + let result: AzureAppConfigKeyValue[] = []; + let currentUrl = url; + + while (currentUrl) { + const res = await request.get<{ items: AzureAppConfigKeyValue[]; ["@nextLink"]: string }>(currentUrl, { + baseURL, + headers: { + Authorization: `Bearer ${accessToken}` + }, + // we force IPV4 because docker setup fails with ipv6 + httpsAgent: new https.Agent({ + family: 4 + }) + }); + + result = result.concat(res.data.items); + currentUrl = res.data?.["@nextLink"]; + } + + return result; + }; + + const $deleteAzureSecret = async (accessToken: string, configurationUrl: string, key: string, label?: string) => { + await request.delete(`${configurationUrl}/kv/${key}?api-version=2023-11-01`, { + headers: { + Authorization: `Bearer ${accessToken}` + }, + ...(label && + label.length > 0 && { + params: { + label + } + }), + httpsAgent: new https.Agent({ + family: 4 + }) + }); + }; + + const syncSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials, secretMap: TSecretMap) => { + if (!secretSync.destinationConfig.configurationUrl.endsWith(".azconfig.io")) { + throw new BadRequestError({ + message: "Invalid Azure App Configuration URL provided." + }); + } + + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigValuesUrlAllSecrets = `/kv?api-version=2023-11-01`; + + const azureAppConfigSecretsLabeled = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrlAllSecrets + ) + ).map((entry) => [ + entry.key, + { + value: entry.value, + label: entry.label + } + ]) + ); + + // add the secrets to azure app config, that are in infisical + for await (const key of Object.keys(secretMap)) { + if (!(key in azureAppConfigSecretsLabeled) || secretMap[key]?.value !== azureAppConfigSecretsLabeled[key]) { + await request.put( + `${secretSync.destinationConfig.configurationUrl}/kv/${key}?api-version=2023-11-01`, + { + value: secretMap[key]?.value, + ...(isAzureKeyVaultReference(secretMap[key]?.value || "") && { + content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" + }) + }, + { + ...(secretSync.destinationConfig.label && { + params: { + label: secretSync.destinationConfig.label + } + }), + + headers: { + Authorization: `Bearer ${accessToken}` + }, + httpsAgent: new https.Agent({ + family: 4 + }) + } + ); + } + } + + for await (const key of Object.keys(azureAppConfigSecrets)) { + const azureSecret = azureAppConfigSecrets[key]; + if ( + !(key in secretMap) || + secretMap[key] === null || + (azureSecret.label && azureSecret.label !== secretSync.destinationConfig.label) || + (!azureSecret.label && secretSync.destinationConfig.label) + ) { + await $deleteAzureSecret(accessToken, secretSync.destinationConfig.configurationUrl, key, azureSecret.label); + } + } + }; + + const removeSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + for await (const infisicalKey of Object.keys(secretMap)) { + if (infisicalKey in azureAppConfigSecrets) { + await $deleteAzureSecret( + accessToken, + secretSync.destinationConfig.configurationUrl, + infisicalKey, + secretSync.destinationConfig.label + ); + } + } + }; + + const getSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const secretMap: TSecretMap = {}; + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + Object.keys(azureAppConfigSecrets).forEach((key) => { + secretMap[key] = { + value: azureAppConfigSecrets[key] + }; + }); + + return secretMap; + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts new file mode 100644 index 000000000..c39581fda --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const AzureAppConfigurationSyncDestinationConfigSchema = z.object({ + configurationUrl: z + .string() + .min(1, "App Configuration URL required") + .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.configurationUrl), + label: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.label) +}); + +const AzureAppConfigurationSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AzureAppConfigurationSyncSchema = BaseSecretSyncSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.AzureAppConfiguration), + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema +}); + +export const CreateAzureAppConfigurationSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema +}); + +export const UpdateAzureAppConfigurationSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema.optional() +}); + +export const AzureAppConfigurationSyncListItemSchema = z.object({ + name: z.literal("Azure App Configuration"), + connection: z.literal(AppConnection.AzureAppConfiguration), + destination: z.literal(SecretSync.AzureAppConfiguration), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts new file mode 100644 index 000000000..4cfbd5472 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TAzureAppConfigurationConnection } from "@app/services/app-connection/azure-app-configuration"; + +import { + AzureAppConfigurationSyncListItemSchema, + AzureAppConfigurationSyncSchema, + CreateAzureAppConfigurationSyncSchema +} from "./azure-app-configuration-sync-schemas"; + +export type TAzureAppConfigurationSync = z.infer; + +export type TAzureAppConfigurationSyncInput = z.infer; + +export type TAzureAppConfigurationSyncListItem = z.infer; + +export type TAzureAppConfigurationSyncWithCredentials = TAzureAppConfigurationSync & { + connection: TAzureAppConfigurationConnection; +}; diff --git a/backend/src/services/secret-sync/azure-app-configuration/index.ts b/backend/src/services/secret-sync/azure-app-configuration/index.ts new file mode 100644 index 000000000..0ed052ff2 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-app-configuration-sync-constants"; +export * from "./azure-app-configuration-sync-fns"; +export * from "./azure-app-configuration-sync-schemas"; +export * from "./azure-app-configuration-sync-types"; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts new file mode 100644 index 000000000..9e2f986ce --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const AZURE_KEY_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Azure Key Vault", + destination: SecretSync.AzureKeyVault, + connection: AppConnection.AzureKeyVault, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts new file mode 100644 index 000000000..21fef297a --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -0,0 +1,253 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { GetAzureKeyVaultSecret, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault-sync-types"; + +type TAzureKeyVaultSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultSyncFactoryDeps) => { + const $getAzureKeyVaultSecrets = async (accessToken: string, vaultBaseUrl: string) => { + const paginateAzureKeyVaultSecrets = async () => { + let result: GetAzureKeyVaultSecret[] = []; + + let currentUrl = `${vaultBaseUrl}/secrets?api-version=7.3`; + + while (currentUrl) { + const res = await request.get<{ value: GetAzureKeyVaultSecret; nextLink: string }>(currentUrl, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + result = result.concat(res.data.value); + currentUrl = res.data.nextLink; + } + + return result; + }; + + const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(); + + const enabledAzureKeyVaultSecrets = getAzureKeyVaultSecrets.filter((secret) => secret.attributes.enabled); + + // disabled keys to skip sending updates to + const disabledAzureKeyVaultSecretKeys = getAzureKeyVaultSecrets + .filter(({ attributes }) => !attributes.enabled) + .map((getAzureKeyVaultSecret) => { + return getAzureKeyVaultSecret.id.substring(getAzureKeyVaultSecret.id.lastIndexOf("/") + 1); + }); + + let lastSlashIndex: number; + const res = ( + await Promise.all( + enabledAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { + if (!lastSlashIndex) { + lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); + } + + const azureKeyVaultSecret = await request.get( + `${getAzureKeyVaultSecret.id}?api-version=7.3`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + return { + ...azureKeyVaultSecret.data, + key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1) + }; + }) + ) + ).reduce( + (obj, secret) => ({ + ...obj, + [secret.key]: secret + }), + {} as Record + ); + + return { + vaultSecrets: res, + disabledAzureKeyVaultSecretKeys + }; + }; + + const syncSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + const setSecrets: { + key: string; + value: string; + }[] = []; + + const deleteSecrets: string[] = []; + + Object.keys(secretMap).forEach((infisicalKey) => { + const hyphenatedKey = infisicalKey.replace(/_/g, "-"); + if (!(hyphenatedKey in vaultSecrets)) { + // case: secret has been created + setSecrets.push({ + key: hyphenatedKey, + value: secretMap[infisicalKey].value + }); + } else if (secretMap[infisicalKey].value !== vaultSecrets[hyphenatedKey].value) { + // case: secret has been updated + setSecrets.push({ + key: hyphenatedKey, + value: secretMap[infisicalKey].value + }); + } + }); + + Object.keys(vaultSecrets).forEach((key) => { + const underscoredKey = key.replace(/-/g, "_"); + if (!(underscoredKey in secretMap)) { + deleteSecrets.push(key); + } + }); + + const setSecretAzureKeyVault = async ({ key, value }: { key: string; value: string }) => { + let isSecretSet = false; + let syncError: Error | null = null; + let maxTries = 6; + if (disabledAzureKeyVaultSecretKeys.includes(key)) return; + + while (!isSecretSet && maxTries > 0) { + // try to set secret + try { + await request.put( + `${secretSync.destinationConfig.vaultBaseUrl}/secrets/${key}?api-version=7.3`, + { + value + }, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + isSecretSet = true; + } catch (err) { + syncError = err as Error; + if (err instanceof AxiosError) { + // eslint-disable-next-line + if (err.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") { + await request.post( + `${secretSync.destinationConfig.vaultBaseUrl}/deletedsecrets/${key}/recover?api-version=7.3`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + await new Promise((resolve) => { + setTimeout(resolve, 10_000); + }); + } else { + await new Promise((resolve) => { + setTimeout(resolve, 10_000); + }); + maxTries -= 1; + } + } + } + } + + if (!isSecretSet) { + throw new SecretSyncError({ + error: syncError, + secretKey: key + }); + } + }; + + for await (const setSecret of setSecrets) { + const { key, value } = setSecret; + await setSecretAzureKeyVault({ + key, + value + }); + } + + for await (const deleteSecretKey of deleteSecrets.filter( + (secret) => !setSecrets.find((setSecret) => setSecret.key === secret) + )) { + await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + } + }; + + const removeSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + for await (const [key] of Object.entries(vaultSecrets)) { + const underscoredKey = key.replace(/-/g, "_"); + + if (underscoredKey in secretMap) { + if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) { + await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${key}?api-version=7.3`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + } + } + } + }; + + const getSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + const secretMap: TSecretMap = {}; + + Object.keys(vaultSecrets).forEach((key) => { + if (!disabledAzureKeyVaultSecretKeys.includes(key)) { + const underscoredKey = key.replace(/-/g, "_"); + secretMap[underscoredKey] = { + value: vaultSecrets[key].value + }; + } + }); + + return secretMap; + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts new file mode 100644 index 000000000..d528f531e --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const AzureKeyVaultSyncDestinationConfigSchema = z.object({ + vaultBaseUrl: z + .string() + .url("Invalid vault base URL format") + .min(1, "Vault base URL required") + .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_KEY_VAULT.vaultBaseUrl) +}); + +const AzureKeyVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AzureKeyVaultSyncSchema = BaseSecretSyncSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.AzureKeyVault), + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema +}); + +export const CreateAzureKeyVaultSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema +}); + +export const UpdateAzureKeyVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema.optional() +}); + +export const AzureKeyVaultSyncListItemSchema = z.object({ + name: z.literal("Azure Key Vault"), + connection: z.literal(AppConnection.AzureKeyVault), + destination: z.literal(SecretSync.AzureKeyVault), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts new file mode 100644 index 000000000..d8083d640 --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +import { TAzureKeyVaultConnection } from "@app/services/app-connection/azure-key-vault"; + +import { + AzureKeyVaultSyncListItemSchema, + AzureKeyVaultSyncSchema, + CreateAzureKeyVaultSyncSchema +} from "./azure-key-vault-sync-schemas"; + +export type TAzureKeyVaultSync = z.infer; + +export type TAzureKeyVaultSyncInput = z.infer; + +export type TAzureKeyVaultSyncListItem = z.infer; + +export type TAzureKeyVaultSyncWithCredentials = TAzureKeyVaultSync & { + connection: TAzureKeyVaultConnection; +}; + +export interface GetAzureKeyVaultSecret { + id: string; // secret URI + value: string; + attributes: { + enabled: boolean; + created: number; + updated: number; + recoveryLevel: string; + recoverableDays: number; + }; +} + +export interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { + key: string; +} diff --git a/backend/src/services/secret-sync/azure-key-vault/index.ts b/backend/src/services/secret-sync/azure-key-vault/index.ts new file mode 100644 index 000000000..f2a7e036f --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-key-vault-sync-constants"; +export * from "./azure-key-vault-sync-fns"; +export * from "./azure-key-vault-sync-schemas"; +export * from "./azure-key-vault-sync-types"; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts b/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts new file mode 100644 index 000000000..b4ee51a04 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const DATABRICKS_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Databricks", + destination: SecretSync.Databricks, + connection: AppConnection.Databricks, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts new file mode 100644 index 000000000..2d450b2b1 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts @@ -0,0 +1,164 @@ +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getDatabricksConnectionAccessToken } from "@app/services/app-connection/databricks"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { + TDatabricksDeleteSecret, + TDatabricksListSecretKeys, + TDatabricksListSecretKeysResponse, + TDatabricksPutSecret, + TDatabricksSyncWithCredentials +} from "@app/services/secret-sync/databricks/databricks-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; + +import { TSecretMap } from "../secret-sync-types"; + +type TDatabricksSecretSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +const DATABRICKS_SCOPE_SECRET_LIMIT = 1000; + +const listDatabricksSecrets = async ({ workspaceUrl, scope, accessToken }: TDatabricksListSecretKeys) => { + const { data } = await request.get( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/list`, + { + params: { + scope + }, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // not present in response if no secrets exist in scope + return data.secrets ?? []; +}; +const putDatabricksSecret = async ({ workspaceUrl, scope, key, value, accessToken }: TDatabricksPutSecret) => + request.post( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/put`, + { + scope, + key, + string_value: value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +const deleteDatabricksSecrets = async ({ workspaceUrl, scope, key, accessToken }: TDatabricksDeleteSecret) => + request.post( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/delete`, + { + scope, + key + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabricksSecretSyncFactoryDeps) => { + const syncSecrets = async (secretSync: TDatabricksSyncWithCredentials, secretMap: TSecretMap) => { + if (Object.keys(secretSync).length > DATABRICKS_SCOPE_SECRET_LIMIT) { + throw new Error( + `Databricks does not support storing more than ${DATABRICKS_SCOPE_SECRET_LIMIT} secrets per scope.` + ); + } + + const { + destinationConfig: { scope }, + connection + } = secretSync; + + const { workspaceUrl } = connection.credentials; + + const accessToken = await getDatabricksConnectionAccessToken(connection, appConnectionDAL, kmsService); + + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + try { + await putDatabricksSecret({ + key, + value, + workspaceUrl, + scope, + accessToken + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + const databricksSecretKeys = await listDatabricksSecrets({ + workspaceUrl, + scope, + accessToken + }); + + for await (const secret of databricksSecretKeys) { + if (!(secret.key in secretMap)) { + await deleteDatabricksSecrets({ + key: secret.key, + workspaceUrl, + scope, + accessToken + }); + } + } + }; + + const removeSecrets = async (secretSync: TDatabricksSyncWithCredentials, secretMap: TSecretMap) => { + const { + destinationConfig: { scope }, + connection + } = secretSync; + + const { workspaceUrl } = connection.credentials; + + const accessToken = await getDatabricksConnectionAccessToken(connection, appConnectionDAL, kmsService); + + const databricksSecretKeys = await listDatabricksSecrets({ + workspaceUrl, + scope, + accessToken + }); + + for await (const secret of databricksSecretKeys) { + if (secret.key in secretMap) { + await deleteDatabricksSecrets({ + key: secret.key, + workspaceUrl, + scope, + accessToken + }); + } + } + }; + + const getSecrets = async (secretSync: TDatabricksSyncWithCredentials) => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts b/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts new file mode 100644 index 000000000..c0f148244 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const DatabricksSyncDestinationConfigSchema = z.object({ + scope: z.string().trim().min(1, "Databricks scope required").describe(SecretSyncs.DESTINATION_CONFIG.DATABRICKS.scope) +}); + +const DatabricksSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const DatabricksSyncSchema = BaseSecretSyncSchema(SecretSync.Databricks, DatabricksSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Databricks), + destinationConfig: DatabricksSyncDestinationConfigSchema +}); + +export const CreateDatabricksSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Databricks, + DatabricksSyncOptionsConfig +).extend({ + destinationConfig: DatabricksSyncDestinationConfigSchema +}); + +export const UpdateDatabricksSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Databricks, + DatabricksSyncOptionsConfig +).extend({ + destinationConfig: DatabricksSyncDestinationConfigSchema.optional() +}); + +export const DatabricksSyncListItemSchema = z.object({ + name: z.literal("Databricks"), + connection: z.literal(AppConnection.Databricks), + destination: z.literal(SecretSync.Databricks), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-types.ts b/backend/src/services/secret-sync/databricks/databricks-sync-types.ts new file mode 100644 index 000000000..41ec06d85 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-types.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { TDatabricksConnection } from "@app/services/app-connection/databricks"; + +import { + CreateDatabricksSyncSchema, + DatabricksSyncListItemSchema, + DatabricksSyncSchema +} from "./databricks-sync-schemas"; + +export type TDatabricksSync = z.infer; + +export type TDatabricksSyncInput = z.infer; + +export type TDatabricksSyncListItem = z.infer; + +export type TDatabricksSyncWithCredentials = TDatabricksSync & { + connection: TDatabricksConnection; +}; + +export type TDatabricksListSecretKeysResponse = { + secrets?: { key: string; last_updated_timestamp: number }[]; +}; + +type TBaseDatabricksSecretRequest = { + scope: string; + workspaceUrl: string; + accessToken: string; +}; + +export type TDatabricksListSecretKeys = TBaseDatabricksSecretRequest; + +export type TDatabricksPutSecret = { + key: string; + value?: string; +} & TBaseDatabricksSecretRequest; + +export type TDatabricksDeleteSecret = { + key: string; +} & TBaseDatabricksSecretRequest; diff --git a/backend/src/services/secret-sync/databricks/index.ts b/backend/src/services/secret-sync/databricks/index.ts new file mode 100644 index 000000000..5b4dec07d --- /dev/null +++ b/backend/src/services/secret-sync/databricks/index.ts @@ -0,0 +1,4 @@ +export * from "./databricks-sync-constants"; +export * from "./databricks-sync-fns"; +export * from "./databricks-sync-schemas"; +export * from "./databricks-sync-types"; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts index b0516d166..0643c431a 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts @@ -1,5 +1,6 @@ import z from "zod"; +import { SecretSyncs } from "@app/lib/api-docs"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { BaseSecretSyncSchema, @@ -14,8 +15,8 @@ import { GcpSyncScope } from "./gcp-sync-enums"; const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; const GcpSyncDestinationConfigSchema = z.object({ - scope: z.literal(GcpSyncScope.Global), - projectId: z.string().min(1, "Project ID is required") + scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), + projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId) }); export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({ diff --git a/backend/src/services/secret-sync/github/github-sync-schemas.ts b/backend/src/services/secret-sync/github/github-sync-schemas.ts index 37a294a1b..76bbc63a7 100644 --- a/backend/src/services/secret-sync/github/github-sync-schemas.ts +++ b/backend/src/services/secret-sync/github/github-sync-schemas.ts @@ -14,21 +14,21 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types" const GitHubSyncDestinationConfigSchema = z .discriminatedUnion("scope", [ z.object({ - scope: z.literal(GitHubSyncScope.Organization), - org: z.string().min(1, "Organization name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.ORG), + scope: z.literal(GitHubSyncScope.Organization).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + org: z.string().min(1, "Organization name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.org), visibility: z.nativeEnum(GitHubSyncVisibility), selectedRepositoryIds: z.number().array().optional() }), z.object({ - scope: z.literal(GitHubSyncScope.Repository), - owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.OWNER), - repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.REPO) + scope: z.literal(GitHubSyncScope.Repository).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.owner), + repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.repo) }), z.object({ - scope: z.literal(GitHubSyncScope.RepositoryEnvironment), - owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.OWNER), - repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.REPO), - env: z.string().min(1, "Environment name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.ENV) + scope: z.literal(GitHubSyncScope.RepositoryEnvironment).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.owner), + repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.repo), + env: z.string().min(1, "Environment name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.env) }) ]) .superRefine((options, ctx) => { diff --git a/backend/src/services/secret-sync/secret-sync-dal.ts b/backend/src/services/secret-sync/secret-sync-dal.ts index 0f5733a3a..cc2cd1fcf 100644 --- a/backend/src/services/secret-sync/secret-sync-dal.ts +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -4,7 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { TSecretSyncs } from "@app/db/schemas/secret-syncs"; import { DatabaseError } from "@app/lib/errors"; -import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex"; +import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; export type TSecretSyncDALFactory = ReturnType; @@ -34,17 +34,9 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt") ); - // prepends table name to filter keys to avoid ambiguous col references, skipping utility filters like $in, etc. - const prependTableName = (filterObj: object): SecretSyncFindFilter => - Object.fromEntries( - Object.entries(filterObj).map(([key, value]) => - key.startsWith("$") ? [key, prependTableName(value as object)] : [`${TableName.SecretSync}.${key}`, value] - ) - ); - if (filter) { /* eslint-disable @typescript-eslint/no-misused-promises */ - void query.where(buildFindFilter(prependTableName(filter))); + void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretSync, filter))); } return query; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 58d68ebb1..743b900bc 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -1,7 +1,11 @@ export enum SecretSync { AWSParameterStore = "aws-parameter-store", + AWSSecretsManager = "aws-secrets-manager", GitHub = "github", - GCPSecretManager = "gcp-secret-manager" + GCPSecretManager = "gcp-secret-manager", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration", + Databricks = "databricks" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index c02b8599d..597c7fc01 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -4,6 +4,11 @@ import { AWS_PARAMETER_STORE_SYNC_LIST_OPTION, AwsParameterStoreSyncFns } from "@app/services/secret-sync/aws-parameter-store"; +import { + AWS_SECRETS_MANAGER_SYNC_LIST_OPTION, + AwsSecretsManagerSyncFns +} from "@app/services/secret-sync/aws-secrets-manager"; +import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks"; import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; @@ -13,19 +18,32 @@ import { TSecretSyncWithCredentials } from "@app/services/secret-sync/secret-sync-types"; +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration"; +import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, + [SecretSync.AWSSecretsManager]: AWS_SECRETS_MANAGER_SYNC_LIST_OPTION, [SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION, - [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION + [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION, + [SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION, + [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, + [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name)); }; +type TSyncSecretDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + // const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { // let secretMap = { ...unprocessedSecretMap }; // @@ -67,34 +85,78 @@ export const listSecretSyncOptions = () => { // }; export const SecretSyncFns = { - syncSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise => { + syncSecrets: ( + secretSync: TSecretSyncWithCredentials, + secretMap: TSecretMap, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { // const affixedSecretMap = addAffixes(secretSync, secretMap); switch (secretSync.destination) { case SecretSync.AWSParameterStore: return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.AWSSecretsManager: + return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.GitHub: return GithubSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.GCPSecretManager: return GcpSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.AzureKeyVault: + return azureKeyVaultSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); + case SecretSync.AzureAppConfiguration: + return azureAppConfigurationSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); + case SecretSync.Databricks: + return databricksSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` ); } }, - getSecrets: async (secretSync: TSecretSyncWithCredentials): Promise => { + getSecrets: async ( + secretSync: TSecretSyncWithCredentials, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { let secretMap: TSecretMap; switch (secretSync.destination) { case SecretSync.AWSParameterStore: secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync); break; + case SecretSync.AWSSecretsManager: + secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync); + break; case SecretSync.GitHub: secretMap = await GithubSyncFns.getSecrets(secretSync); break; case SecretSync.GCPSecretManager: secretMap = await GcpSyncFns.getSecrets(secretSync); break; + case SecretSync.AzureKeyVault: + secretMap = await azureKeyVaultSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; + case SecretSync.AzureAppConfiguration: + secretMap = await azureAppConfigurationSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; + case SecretSync.Databricks: + return databricksSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -104,16 +166,37 @@ export const SecretSyncFns = { return secretMap; // return stripAffixes(secretSync, secretMap); }, - removeSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise => { + removeSecrets: ( + secretSync: TSecretSyncWithCredentials, + secretMap: TSecretMap, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { // const affixedSecretMap = addAffixes(secretSync, secretMap); switch (secretSync.destination) { case SecretSync.AWSParameterStore: return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.AWSSecretsManager: + return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.GitHub: return GithubSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.GCPSecretManager: return GcpSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.AzureKeyVault: + return azureKeyVaultSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); + case SecretSync.AzureAppConfiguration: + return azureAppConfigurationSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); + case SecretSync.Databricks: + return databricksSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -122,17 +205,25 @@ export const SecretSyncFns = { } }; +const MAX_MESSAGE_LENGTH = 1024; + export const parseSyncErrorMessage = (err: unknown): string => { + let errorMessage: string; + if (err instanceof SecretSyncError) { - return JSON.stringify({ + errorMessage = JSON.stringify({ secretKey: err.secretKey, error: err.message || parseSyncErrorMessage(err.error) }); + } else if (err instanceof AxiosError) { + errorMessage = err?.response?.data + ? JSON.stringify(err?.response?.data) + : err?.message ?? "An unknown error occurred."; + } else { + errorMessage = (err as Error)?.message || "An unknown error occurred."; } - if (err instanceof AxiosError) { - return err?.response?.data ? JSON.stringify(err?.response?.data) : err?.message ?? "An unknown error occurred."; - } - - return (err as Error)?.message || "An unknown error occurred."; + return errorMessage.length <= MAX_MESSAGE_LENGTH + ? errorMessage + : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; }; diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 2df7d11e6..92546706d 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -3,12 +3,20 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AWSParameterStore]: "AWS Parameter Store", + [SecretSync.AWSSecretsManager]: "AWS Secrets Manager", [SecretSync.GitHub]: "GitHub", - [SecretSync.GCPSecretManager]: "GCP Secret Manager" + [SecretSync.GCPSecretManager]: "GCP Secret Manager", + [SecretSync.AzureKeyVault]: "Azure Key Vault", + [SecretSync.AzureAppConfiguration]: "Azure App Configuration", + [SecretSync.Databricks]: "Databricks" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, + [SecretSync.AWSSecretsManager]: AppConnection.AWS, [SecretSync.GitHub]: AppConnection.GitHub, - [SecretSync.GCPSecretManager]: AppConnection.GCP + [SecretSync.GCPSecretManager]: AppConnection.GCP, + [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, + [SecretSync.Databricks]: AppConnection.Databricks }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index d2bcdb590..808930b15 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -57,11 +57,14 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; + export type TSecretSyncQueueFactory = ReturnType; type TSecretSyncQueueFactoryDep = { queueService: Pick; kmsService: Pick; + appConnectionDAL: Pick; keyStore: Pick; folderDAL: TSecretFolderDALFactory; secretV2BridgeDAL: Pick< @@ -111,6 +114,7 @@ const getRequeueDelay = (failureCount?: number) => { export const secretSyncQueueFactory = ({ queueService, kmsService, + appConnectionDAL, keyStore, folderDAL, secretV2BridgeDAL, @@ -322,7 +326,10 @@ export const secretSyncQueueFactory = ({ "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path." ); - const importedSecrets = await SecretSyncFns.getSecrets(secretSync); + const importedSecrets = await SecretSyncFns.getSecrets(secretSync, { + appConnectionDAL, + kmsService + }); if (!Object.keys(importedSecrets).length) return {}; @@ -434,7 +441,10 @@ export const secretSyncQueueFactory = ({ }); } - await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap); + await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, { + appConnectionDAL, + kmsService + }); isSynced = true; } catch (err) { @@ -672,7 +682,11 @@ export const secretSyncQueueFactory = ({ credentials } } as TSecretSyncWithCredentials, - secretMap + secretMap, + { + appConnectionDAL, + kmsService + } ); isSuccess = true; diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 9821c4e1d..89bdc4375 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -13,7 +13,7 @@ const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = initialSyncBehavior: (options.canImportSecrets ? z.nativeEnum(SecretSyncInitialSyncBehavior) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) - ).describe(SecretSyncs.SYNC_OPTIONS(secretSync).INITIAL_SYNC_BEHAVIOR) + ).describe(SecretSyncs.SYNC_OPTIONS(secretSync).initialSyncBehavior) // prependPrefix: z // .string() // .trim() diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index fb622ee9a..2c6d3a830 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -2,6 +2,18 @@ import { Job } from "bullmq"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { QueueJobs } from "@app/queue"; +import { + TAwsSecretsManagerSync, + TAwsSecretsManagerSyncInput, + TAwsSecretsManagerSyncListItem, + TAwsSecretsManagerSyncWithCredentials +} from "@app/services/secret-sync/aws-secrets-manager"; +import { + TDatabricksSync, + TDatabricksSyncInput, + TDatabricksSyncListItem, + TDatabricksSyncWithCredentials +} from "@app/services/secret-sync/databricks"; import { TGitHubSync, TGitHubSyncInput, @@ -17,18 +29,55 @@ import { TAwsParameterStoreSyncListItem, TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store"; +import { + TAzureAppConfigurationSync, + TAzureAppConfigurationSyncInput, + TAzureAppConfigurationSyncListItem, + TAzureAppConfigurationSyncWithCredentials +} from "./azure-app-configuration"; +import { + TAzureKeyVaultSync, + TAzureKeyVaultSyncInput, + TAzureKeyVaultSyncListItem, + TAzureKeyVaultSyncWithCredentials +} from "./azure-key-vault"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; -export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync; +export type TSecretSync = + | TAwsParameterStoreSync + | TAwsSecretsManagerSync + | TGitHubSync + | TGcpSync + | TAzureKeyVaultSync + | TAzureAppConfigurationSync + | TDatabricksSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials + | TAwsSecretsManagerSyncWithCredentials | TGitHubSyncWithCredentials - | TGcpSyncWithCredentials; + | TGcpSyncWithCredentials + | TAzureKeyVaultSyncWithCredentials + | TAzureAppConfigurationSyncWithCredentials + | TDatabricksSyncWithCredentials; -export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput | TGcpSyncInput; +export type TSecretSyncInput = + | TAwsParameterStoreSyncInput + | TAwsSecretsManagerSyncInput + | TGitHubSyncInput + | TGcpSyncInput + | TAzureKeyVaultSyncInput + | TAzureAppConfigurationSyncInput + | TDatabricksSyncInput; -export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem | TGcpSyncListItem; +export type TSecretSyncListItem = + | TAwsParameterStoreSyncListItem + | TAwsSecretsManagerSyncListItem + | TGitHubSyncListItem + | TGcpSyncListItem + | TAzureKeyVaultSyncListItem + | TAzureAppConfigurationSyncListItem + | TDatabricksSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 64ad77deb..99980fba7 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -414,6 +414,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretTag}.id` ) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) + .where((qb) => { + if (filters?.metadataFilter && filters.metadataFilter.length > 0) { + filters.metadataFilter.forEach((meta) => { + void qb.whereExists((subQuery) => { + void subQuery + .select("secretId") + .from(TableName.ResourceMetadata) + .whereRaw(`"${TableName.ResourceMetadata}"."secretId" = "${TableName.SecretV2}"."id"`) + .where(`${TableName.ResourceMetadata}.key`, meta.key) + .where(`${TableName.ResourceMetadata}.value`, meta.value); + }); + }); + } + }) .select( selectAllTableCols(TableName.SecretV2), db.raw( @@ -481,6 +495,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { } ] }); + return data; } catch (error) { throw new DatabaseError({ error, name: "get all secret" }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index e9e04dbd8..b03a7a090 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1291,8 +1291,13 @@ export const secretV2BridgeServiceFactory = ({ ] } }); - if (secretsToUpdate.length !== inputSecrets.length) - throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` }); + if (secretsToUpdate.length !== inputSecrets.length) { + const secretsToUpdateNames = secretsToUpdate.map((secret) => secret.key); + const invalidSecrets = inputSecrets.filter((secret) => !secretsToUpdateNames.includes(secret.secretKey)); + throw new NotFoundError({ + message: `Secret does not exist: ${invalidSecrets.map((el) => el.secretKey).join(",")}` + }); + } const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key); secretsToUpdate.forEach((el) => { diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 5e5cc26cd..37512d93a 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -30,6 +30,10 @@ export type TGetSecretsDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + metadataFilter?: { + key?: string; + value?: string; + }[]; orderBy?: SecretsOrderBy; orderDirection?: OrderByDirection; offset?: number; @@ -310,6 +314,7 @@ export type TFindSecretsByFolderIdsFilter = { orderDirection?: OrderByDirection; search?: string; tagSlugs?: string[]; + metadataFilter?: { key?: string; value?: string }[]; includeTagsInSearch?: boolean; keys?: string[]; }; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index dc973c0b1..00b0e7da8 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1488,7 +1488,18 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.SecretWebhook, async (job) => { - await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: job.data.projectId + }); + + await fnTriggerWebhook({ + ...job.data, + projectEnvDAL, + webhookDAL, + projectDAL, + secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() + }); }); return { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index a9bba106c..e58615b0f 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1263,6 +1263,13 @@ export const secretServiceFactory = ({ name: "bot_not_found_error" }); + if (paramsV2.metadataFilter) { + throw new BadRequestError({ + message: "Please upgrade your project to filter secrets by metadata", + name: "SecretMetadataNotSupported" + }); + } + const { secrets, imports } = await getSecrets({ actorId, projectId, @@ -1444,7 +1451,7 @@ export const secretServiceFactory = ({ decryptedSecret.secretValue = expandedSecretValue || ""; } - return decryptedSecret; + return { secretMetadata: undefined, ...decryptedSecret }; }; const createSecretRaw = async ({ diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index d1011bf8c..a0082efc6 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -182,6 +182,10 @@ export type TGetSecretsRawDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + metadataFilter?: { + key?: string; + value?: string; + }[]; orderBy?: SecretsOrderBy; orderDirection?: OrderByDirection; offset?: number; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b0fdd9c5c..9a6075423 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -2,7 +2,7 @@ import bcrypt from "bcrypt"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; @@ -87,17 +87,21 @@ export const superAdminServiceFactory = ({ // reset on initialized await keyStore.deleteItem(ADMIN_CONFIG_KEY); - const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); - if (serverCfg) return; + const serverCfg = await serverCfgDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SuperAdminInit]); + const serverCfgInDB = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (serverCfgInDB) return serverCfgInDB; - const newCfg = await serverCfgDAL.create({ - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - id: ADMIN_CONFIG_DB_UUID, - initialized: false, - allowSignUp: true, - defaultAuthOrgId: null + const newCfg = await serverCfgDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: ADMIN_CONFIG_DB_UUID, + initialized: false, + allowSignUp: true, + defaultAuthOrgId: null + }); + return newCfg; }); - return newCfg; + return serverCfg; }; const updateServerCfg = async ( diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 58f51f880..e46f9db2a 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -3,9 +3,8 @@ import crypto from "node:crypto"; import { AxiosError } from "axios"; import picomatch from "picomatch"; -import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas"; +import { TWebhooks } from "@app/db/schemas"; import { request } from "@app/lib/config/request"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -16,28 +15,14 @@ import { WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; -export const decryptWebhookDetails = (webhook: TWebhooks) => { - const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook; +export const decryptWebhookDetails = (webhook: TWebhooks, decryptor: (value: Buffer) => string) => { + const { encryptedPassKey, encryptedUrl } = webhook; + + const decryptedUrl = decryptor(encryptedUrl); let decryptedSecretKey = ""; - let decryptedUrl = url; - - if (encryptedSecretKey) { - decryptedSecretKey = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: encryptedSecretKey, - iv: iv as string, - tag: tag as string - }); - } - - if (urlCipherText) { - decryptedUrl = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: urlCipherText, - iv: urlIV as string, - tag: urlTag as string - }); + if (encryptedPassKey) { + decryptedSecretKey = decryptor(encryptedPassKey); } return { @@ -46,10 +31,14 @@ export const decryptWebhookDetails = (webhook: TWebhooks) => { }; }; -export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record) => { +export const triggerWebhookRequest = async ( + webhook: TWebhooks, + decryptor: (value: Buffer) => string, + data: Record +) => { const headers: Record = {}; const payload = { ...data, timestamp: Date.now() }; - const { secretKey, url } = decryptWebhookDetails(webhook); + const { secretKey, url } = decryptWebhookDetails(webhook, decryptor); if (secretKey) { const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); @@ -124,6 +113,7 @@ export type TFnTriggerWebhookDTO = { webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; + secretManagerDecryptor: (value: Buffer) => string; }; // this is reusable function @@ -134,7 +124,8 @@ export const fnTriggerWebhook = async ({ projectId, webhookDAL, projectEnvDAL, - projectDAL + projectDAL, + secretManagerDecryptor }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -148,6 +139,7 @@ export const fnTriggerWebhook = async ({ toBeTriggeredHooks.map((hook) => triggerWebhookRequest( hook, + secretManagerDecryptor, getWebhookPayload("secrets.modified", { workspaceName: project.name, workspaceId: projectId, diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index 26136aaf6..bb078e0f1 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -3,9 +3,10 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, TWebhooksInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { NotFoundError } from "@app/lib/errors"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; @@ -23,6 +24,7 @@ type TWebhookServiceFactoryDep = { projectEnvDAL: TProjectEnvDALFactory; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; }; export type TWebhookServiceFactory = ReturnType; @@ -31,7 +33,8 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService, - projectDAL + projectDAL, + kmsService }: TWebhookServiceFactoryDep) => { const createWebhook = async ({ actor, @@ -60,30 +63,20 @@ export const webhookServiceFactory = ({ message: `Environment with slug '${environment}' in project with ID '${projectId}' not found` }); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const insertDoc: TWebhooksInsert = { - url: "", // deprecated - we are moving away from plaintext URLs envId: env.id, isDisabled: false, secretPath: secretPath || "/", - type + type, + encryptedUrl: secretManagerEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob }; if (webhookSecretKey) { - const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey); - insertDoc.encryptedSecretKey = ciphertext; - insertDoc.iv = iv; - insertDoc.tag = tag; - insertDoc.algorithm = algorithm; - insertDoc.keyEncoding = encoding; - } - - if (webhookUrl) { - const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl); - insertDoc.urlCipherText = ciphertext; - insertDoc.urlIV = iv; - insertDoc.urlTag = tag; - insertDoc.algorithm = algorithm; - insertDoc.keyEncoding = encoding; + insertDoc.encryptedPassKey = secretManagerEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob; } const webhook = await webhookDAL.create(insertDoc); @@ -140,12 +133,17 @@ export const webhookServiceFactory = ({ }); const project = await projectDAL.findById(webhook.projectId); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: project.id + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); let webhookError: string | undefined; try { await triggerWebhookRequest( webhook, + (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(), getWebhookPayload("test", { workspaceName: project.name, workspaceId: webhook.projectId, @@ -185,8 +183,13 @@ export const webhookServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + return webhooks.map((w) => { - const { url } = decryptWebhookDetails(w); + const { url } = decryptWebhookDetails(w, (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()); return { ...w, url diff --git a/backend/tsconfig.json b/backend/tsconfig.json index fcf508922..90165acbe 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,7 +1,8 @@ { "ts-node": { // Do not forget to `npm i -D tsconfig-paths` - "require": ["tsconfig-paths/register"] + "require": ["tsconfig-paths/register"], + "files": true }, "compilerOptions": { "target": "esnext", @@ -19,6 +20,7 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, "skipLibCheck": true, "baseUrl": ".", "paths": { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 40d17c1b0..680a655d8 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -56,20 +56,6 @@ services: POSTGRES_USER: infisical POSTGRES_DB: infisical-test - db-migration: - container_name: infisical-db-migration - depends_on: - - db - build: - context: ./backend - dockerfile: Dockerfile.dev - env_file: .env - environment: - - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable - command: npm run migration:latest - volumes: - - ./backend/src:/app/src - backend: container_name: infisical-dev-api build: @@ -80,8 +66,6 @@ services: condition: service_started redis: condition: service_started - db-migration: - condition: service_completed_successfully env_file: - .env ports: @@ -192,7 +176,7 @@ services: depends_on: - openldap profiles: [ldap] - + keycloak: image: quay.io/keycloak/keycloak:26.1.0 restart: always @@ -202,7 +186,7 @@ services: command: start-dev ports: - 8088:8080 - profiles: [ sso ] + profiles: [sso] volumes: postgres-data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 77a1e04ab..d3526d841 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,18 +1,6 @@ version: "3" services: - db-migration: - container_name: infisical-db-migration - depends_on: - db: - condition: service_healthy - image: infisical/infisical:latest-postgres - env_file: .env - command: npm run migration:latest - pull_policy: always - networks: - - infisical - backend: container_name: infisical-backend restart: unless-stopped @@ -21,8 +9,6 @@ services: condition: service_healthy redis: condition: service_started - db-migration: - condition: service_completed_successfully image: infisical/infisical:latest-postgres pull_policy: always env_file: .env @@ -69,4 +55,5 @@ volumes: driver: local networks: - infisical: \ No newline at end of file + infisical: + diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx new file mode 100644 index 000000000..03d5f3537 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/azure-app-configuration/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx new file mode 100644 index 000000000..6c429c369 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/azure-app-configuration" +--- + + + Azure App Configuration Connections must be created through the Infisical UI. + Check out the configuration docs for [Azure App Configuration Connections](/integrations/app-connections/azure-app-configuration) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx new file mode 100644 index 000000000..cc2e4ca38 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx new file mode 100644 index 000000000..c49afe500 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx new file mode 100644 index 000000000..a38365b01 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/azure-app-configuration/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx new file mode 100644 index 000000000..4da96476d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx new file mode 100644 index 000000000..65d29899e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- + + + Azure App Configuration Connections must be updated through the Infisical UI. + Check out the configuration docs for [Azure App Configuration Connections](/integrations/app-connections/azure-app-configuration) for a step-by-step + guide. + diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx new file mode 100644 index 000000000..4b1c758ee --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/azure-key-vault/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx new file mode 100644 index 000000000..d0d9f7e6f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/azure-key-vault" +--- + + + Azure Key Vault Connections must be created through the Infisical UI. + Check out the configuration docs for [Azure Key Vault Connections](/integrations/app-connections/azure-key-vault) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx new file mode 100644 index 000000000..02fbe4a31 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/azure-key-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx new file mode 100644 index 000000000..e5d3e77a1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/azure-key-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx new file mode 100644 index 000000000..55502def8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/azure-key-vault/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx new file mode 100644 index 000000000..76f1f8b88 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx new file mode 100644 index 000000000..8637b838a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/azure-key-vault/{connectionId}" +--- + + + Azure Key Vault Connections must be updated through the Infisical UI. + Check out the configuration docs for [Azure Key Vault Connections](/integrations/app-connections/azure-key-vault) for a step-by-step + guide. + diff --git a/docs/api-reference/endpoints/app-connections/databricks/available.mdx b/docs/api-reference/endpoints/app-connections/databricks/available.mdx new file mode 100644 index 000000000..6c277f702 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/databricks/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/create.mdx b/docs/api-reference/endpoints/app-connections/databricks/create.mdx new file mode 100644 index 000000000..5361acb4b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/databricks" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/delete.mdx b/docs/api-reference/endpoints/app-connections/databricks/delete.mdx new file mode 100644 index 000000000..fba97fb14 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx new file mode 100644 index 000000000..1f861328c --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx new file mode 100644 index 000000000..f89c8a8d7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/databricks/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/list.mdx b/docs/api-reference/endpoints/app-connections/databricks/list.mdx new file mode 100644 index 000000000..1449b5166 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/databricks" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/update.mdx b/docs/api-reference/endpoints/app-connections/databricks/update.mdx new file mode 100644 index 000000000..69ddbf617 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/kms/keys/get-by-id.mdx b/docs/api-reference/endpoints/kms/keys/get-by-id.mdx new file mode 100644 index 000000000..a896c640e --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Key by ID" +openapi: "Get /api/v1/kms/keys/{keyId}" +--- diff --git a/docs/api-reference/endpoints/kms/keys/get-by-name.mdx b/docs/api-reference/endpoints/kms/keys/get-by-name.mdx new file mode 100644 index 000000000..fe94da19b --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Key by Name" +openapi: "Get /api/v1/kms/keys/key-name/{keyName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx new file mode 100644 index 000000000..c7fbe5a19 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx new file mode 100644 index 000000000..aacd95571 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx new file mode 100644 index 000000000..31805aaf6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx new file mode 100644 index 000000000..4eac37dc2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx new file mode 100644 index 000000000..c3cc65be1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx new file mode 100644 index 000000000..1775ba450 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx new file mode 100644 index 000000000..5398766b9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx new file mode 100644 index 000000000..9119f6147 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx new file mode 100644 index 000000000..6d886630d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx new file mode 100644 index 000000000..82456fb6c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx new file mode 100644 index 000000000..23c2ad27b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx new file mode 100644 index 000000000..45418f047 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx new file mode 100644 index 000000000..488a15974 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx new file mode 100644 index 000000000..b44944951 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx new file mode 100644 index 000000000..2ea72fb20 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx new file mode 100644 index 000000000..952a852d2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx new file mode 100644 index 000000000..e3d37dd56 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx new file mode 100644 index 000000000..b22a302dc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx new file mode 100644 index 000000000..493e41abd --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx new file mode 100644 index 000000000..7b02b6f68 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx new file mode 100644 index 000000000..5b8ecd8c7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx new file mode 100644 index 000000000..dbb7b6f5c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/azure-key-vault/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx new file mode 100644 index 000000000..08ac487c9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx new file mode 100644 index 000000000..a462739f7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx new file mode 100644 index 000000000..8882c5e47 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx new file mode 100644 index 000000000..87f6b4f56 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx new file mode 100644 index 000000000..2d390ab8b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx new file mode 100644 index 000000000..ba91528f5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/databricks" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx new file mode 100644 index 000000000..862681613 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx new file mode 100644 index 000000000..7cf8fed53 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx new file mode 100644 index 000000000..fe2d239ff --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/databricks/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx new file mode 100644 index 000000000..dd705408f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/databricks" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx new file mode 100644 index 000000000..5e4e69fce --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/databricks/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx new file mode 100644 index 000000000..002fea158 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/databricks/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx new file mode 100644 index 000000000..4a43311a3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index e5bec4d6c..a20f52171 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -4,6 +4,18 @@ title: "Changelog" The changelog below reflects new product developments and updates on a monthly basis. +## January 2025 + +- Released new integration architecture with decoupled authentication, replacing native integrations with [App Connections](https://infisical.com/docs/integrations/app-connections/overview) and [Secret Syncs](https://infisical.com/docs/integrations/secret-syncs/overview). Initial support for AWS Parameter Store, GitHub, and GCP Secret Manager with improved API and Terraform integration capabilities. +- Added support for OIDC group mapping in [Keycloak](https://infisical.com/docs/documentation/platform/sso/keycloak-oidc/overview), enabling automatic mapping of Keycloak groups to Infisical for role-based access control. +- Enhanced [Kubernetes operator](https://infisical.com/docs/integrations/platforms/kubernetes/overview#kubernetes-operator) with namespaced group support, bi-directional secret sync (push to Infisical), [dynamic secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview#dynamic-secrets) capabilities, and support for multiple operator instances. +- Restructured navigation with dedicated sections for Secrets Management, [Certificate Management (PKI)](https://infisical.com/docs/documentation/platform/pki/overview), [Key Management (KMS)](https://infisical.com/docs/documentation/platform/kms/overview#key-management-service-kms), and [SSH Key Management](https://infisical.com/docs/documentation/platform/ssh). +- Added [ephemeral Terraform resource](https://infisical.com/docs/integrations/frameworks/terraform#terraform-provider) support and improved secret sync architecture. +- Released [.NET provider](https://github.com/Infisical/infisical-dotnet-configuration) with first-party Azure authentication support and Azure CLI integration. +- Implemented secret Access Visibility allowing users to view all entities with access to specific secrets in the secret side panel. +- Added secret filtering by metadata and SSH assigned certificates (Version 1). + + ## December 2024 - Added [GCP KMS](https://infisical.com/docs/documentation/platform/kms/overview) integration support. - Added support for [K8s CSI integration](https://infisical.com/docs/integrations/platforms/kubernetes-csi) and ability to point K8s operator to specific secret versions. @@ -12,7 +24,7 @@ The changelog below reflects new product developments and updates on a monthly b - Added Group View Page for improved team management. - Added instance URL to email verification for Infisical accounts. - Added ability to copy full path of nested folders. -- Added custom templating support for K8s operator, allowing flexible secret key mapping and additional fields +- Added custom templating support for K8s operator, allowing flexible secret key mapping and additional fields. - Optimized secrets versions table performance. ## November 2024 diff --git a/docs/contributing/platform/backend/how-to-create-a-feature.mdx b/docs/contributing/platform/backend/how-to-create-a-feature.mdx index 8449eb501..f02040cfa 100644 --- a/docs/contributing/platform/backend/how-to-create-a-feature.mdx +++ b/docs/contributing/platform/backend/how-to-create-a-feature.mdx @@ -4,9 +4,6 @@ title: "Backend development guide" Suppose you're interested in implementing a new feature in Infisical's backend, let's call it "feature-x." Here are the general steps you should follow. -## Database schema migration -In order to run [schema migrations](https://en.wikipedia.org/wiki/Schema_migration#:~:text=A%20schema%20migration%20is%20performed,some%20newer%20or%20older%20version) you need to expose your database connection string. Create a `.env.migration` file to set the database connection URI for migration scripts, or alternatively, export the `DB_CONNECTION_URI` environment variable. - ## Creating new database model If your feature involves a change in the database, you need to first address this by generating the necessary database schemas. diff --git a/docs/images/app-connections/azure/app-configuration/create-oauth-method.png b/docs/images/app-connections/azure/app-configuration/create-oauth-method.png new file mode 100644 index 000000000..1c8ed186b Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/create-oauth-method.png differ diff --git a/docs/images/app-connections/azure/app-configuration/oauth-connection.png b/docs/images/app-connections/azure/app-configuration/oauth-connection.png new file mode 100644 index 000000000..906b30899 Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/oauth-connection.png differ diff --git a/docs/images/app-connections/azure/app-configuration/select-connection.png b/docs/images/app-connections/azure/app-configuration/select-connection.png new file mode 100644 index 000000000..ff452fb8b Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/select-connection.png differ diff --git a/docs/images/app-connections/azure/grant-access.png b/docs/images/app-connections/azure/grant-access.png new file mode 100644 index 000000000..c0545a343 Binary files /dev/null and b/docs/images/app-connections/azure/grant-access.png differ diff --git a/docs/images/app-connections/azure/key-vault/create-oauth-method.png b/docs/images/app-connections/azure/key-vault/create-oauth-method.png new file mode 100644 index 000000000..6d7de342a Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/create-oauth-method.png differ diff --git a/docs/images/app-connections/azure/key-vault/oauth-connection.png b/docs/images/app-connections/azure/key-vault/oauth-connection.png new file mode 100644 index 000000000..5c0239cdc Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/oauth-connection.png differ diff --git a/docs/images/app-connections/azure/key-vault/select-connection.png b/docs/images/app-connections/azure/key-vault/select-connection.png new file mode 100644 index 000000000..e55028588 Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/select-connection.png differ diff --git a/docs/images/app-connections/azure/keyvault-azure-permissions.png b/docs/images/app-connections/azure/keyvault-azure-permissions.png new file mode 100644 index 000000000..0f009cdc1 Binary files /dev/null and b/docs/images/app-connections/azure/keyvault-azure-permissions.png differ diff --git a/docs/images/app-connections/azure/register-callback.png b/docs/images/app-connections/azure/register-callback.png new file mode 100644 index 000000000..b06b5ce6f Binary files /dev/null and b/docs/images/app-connections/azure/register-callback.png differ diff --git a/docs/images/app-connections/databricks/add-service-principal.png b/docs/images/app-connections/databricks/add-service-principal.png new file mode 100644 index 000000000..0d695582d Binary files /dev/null and b/docs/images/app-connections/databricks/add-service-principal.png differ diff --git a/docs/images/app-connections/databricks/create-databricks-service-principal-method.png b/docs/images/app-connections/databricks/create-databricks-service-principal-method.png new file mode 100644 index 000000000..b8dd38bf1 Binary files /dev/null and b/docs/images/app-connections/databricks/create-databricks-service-principal-method.png differ diff --git a/docs/images/app-connections/databricks/create-service-principal.png b/docs/images/app-connections/databricks/create-service-principal.png new file mode 100644 index 000000000..145931442 Binary files /dev/null and b/docs/images/app-connections/databricks/create-service-principal.png differ diff --git a/docs/images/app-connections/databricks/databricks-service-principal-connection.png b/docs/images/app-connections/databricks/databricks-service-principal-connection.png new file mode 100644 index 000000000..6b632553c Binary files /dev/null and b/docs/images/app-connections/databricks/databricks-service-principal-connection.png differ diff --git a/docs/images/app-connections/databricks/manage-service-principals.png b/docs/images/app-connections/databricks/manage-service-principals.png new file mode 100644 index 000000000..f76d2400d Binary files /dev/null and b/docs/images/app-connections/databricks/manage-service-principals.png differ diff --git a/docs/images/app-connections/databricks/select-databricks-connection.png b/docs/images/app-connections/databricks/select-databricks-connection.png new file mode 100644 index 000000000..41a21e6f4 Binary files /dev/null and b/docs/images/app-connections/databricks/select-databricks-connection.png differ diff --git a/docs/images/app-connections/databricks/service-principal-ids.png b/docs/images/app-connections/databricks/service-principal-ids.png new file mode 100644 index 000000000..2748c13e8 Binary files /dev/null and b/docs/images/app-connections/databricks/service-principal-ids.png differ diff --git a/docs/images/app-connections/databricks/service-principal-secrets.png b/docs/images/app-connections/databricks/service-principal-secrets.png new file mode 100644 index 000000000..e1e0b053e Binary files /dev/null and b/docs/images/app-connections/databricks/service-principal-secrets.png differ diff --git a/docs/images/app-connections/databricks/workspace-settings.png b/docs/images/app-connections/databricks/workspace-settings.png new file mode 100644 index 000000000..5c3ec54a6 Binary files /dev/null and b/docs/images/app-connections/databricks/workspace-settings.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png new file mode 100644 index 000000000..d788d27f9 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png new file mode 100644 index 000000000..07bbf6212 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png new file mode 100644 index 000000000..a958af9fd Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png new file mode 100644 index 000000000..abd2c0ad1 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png new file mode 100644 index 000000000..ddffa9190 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png new file mode 100644 index 000000000..8da1d5601 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png b/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png new file mode 100644 index 000000000..7d32012c1 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png b/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png new file mode 100644 index 000000000..22610ae22 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-details.png b/docs/images/secret-syncs/azure-app-configuration/app-config-details.png new file mode 100644 index 000000000..e007bd7f5 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-details.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-options.png b/docs/images/secret-syncs/azure-app-configuration/app-config-options.png new file mode 100644 index 000000000..94b70a547 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-options.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-review.png b/docs/images/secret-syncs/azure-app-configuration/app-config-review.png new file mode 100644 index 000000000..db10d6d41 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-review.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-source.png b/docs/images/secret-syncs/azure-app-configuration/app-config-source.png new file mode 100644 index 000000000..df2e8b22f Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-source.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png b/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png new file mode 100644 index 000000000..1d229319f Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/select-app-config.png b/docs/images/secret-syncs/azure-app-configuration/select-app-config.png new file mode 100644 index 000000000..42ad36997 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/select-app-config.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png b/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png new file mode 100644 index 000000000..6380b315b Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-destination.png b/docs/images/secret-syncs/azure-key-vault/vault-destination.png new file mode 100644 index 000000000..636c892f4 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-destination.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-details.png b/docs/images/secret-syncs/azure-key-vault/vault-details.png new file mode 100644 index 000000000..fdaa51ec8 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-details.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-options.png b/docs/images/secret-syncs/azure-key-vault/vault-options.png new file mode 100644 index 000000000..f35d1cc3b Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-options.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-review.png b/docs/images/secret-syncs/azure-key-vault/vault-review.png new file mode 100644 index 000000000..51572a4ca Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-review.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-source.png b/docs/images/secret-syncs/azure-key-vault/vault-source.png new file mode 100644 index 000000000..5e5e624f1 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-source.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-synced.png b/docs/images/secret-syncs/azure-key-vault/vault-synced.png new file mode 100644 index 000000000..828aa42a8 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-synced.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-created.png b/docs/images/secret-syncs/databricks/databricks-created.png new file mode 100644 index 000000000..829c5196c Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-created.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-destination.png b/docs/images/secret-syncs/databricks/databricks-destination.png new file mode 100644 index 000000000..8b76f8f65 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-destination.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-details.png b/docs/images/secret-syncs/databricks/databricks-details.png new file mode 100644 index 000000000..71630f158 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-details.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-options.png b/docs/images/secret-syncs/databricks/databricks-options.png new file mode 100644 index 000000000..76890d183 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-options.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-review.png b/docs/images/secret-syncs/databricks/databricks-review.png new file mode 100644 index 000000000..e7ff5f6c1 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-review.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-source.png b/docs/images/secret-syncs/databricks/databricks-source.png new file mode 100644 index 000000000..74e9e3535 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-source.png differ diff --git a/docs/images/secret-syncs/databricks/select-databricks-option.png b/docs/images/secret-syncs/databricks/select-databricks-option.png new file mode 100644 index 000000000..26f2fd4bd Binary files /dev/null and b/docs/images/secret-syncs/databricks/select-databricks-option.png differ diff --git a/docs/integrations/app-connections/azure-app-configuration.mdx b/docs/integrations/app-connections/azure-app-configuration.mdx new file mode 100644 index 000000000..959a1812a --- /dev/null +++ b/docs/integrations/app-connections/azure-app-configuration.mdx @@ -0,0 +1,90 @@ +--- +title: "Azure App Configuration Connection" +description: "Learn how to configure a Azure App Configuration Connection for Infisical." +--- + +Infisical currently only supports one method for connecting to Azure, which is OAuth. + + + Using the Azure App Configuration connection on a self-hosted instance of Infisical requires configuring an application in Azure + and registering your instance with it. + + **Prerequisites:** + + - Set up Azure and have an existing App Configuration instance. + + + + Navigate to Azure Active Directory > App registrations to create a new application. + + + Azure Active Directory is now Microsoft Entra ID. + + ![Azure app config](/images/integrations/azure-app-configuration/config-aad.png) + ![Azure app config](/images/integrations/azure-app-configuration/config-new-app.png) + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/azure/oauth/callback`. + + The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance. + + + ![Azure app config](/images/app-connections/azure/register-callback.png) + + + + For the Azure Connection to work with App Configuration, you need to assign multiple permissions to the application. + + #### Azure App Configuration permissions + + Set the API permissions of the Azure application to include the following Azure App Configuration permissions: `KeyValue.Delete`, `KeyValue.Read`, and `KeyValue.Write`. + ![Azure app config](../../images/integrations/azure-app-configuration/app-api-permissions.png) + + + + + Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. + + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-1.png) + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-2.png) + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-3.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. + + - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + + Once added, restart your Infisical instance and use the Azure App Configuration connection. + + + + + +## Setup Azure Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/app-configuration/select-connection.png) + + + You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID. + + Now select the **OAuth** method and click **Connect to Azure**. + + ![Connect via Azure OAUth](/images/app-connections/azure/app-configuration/create-oauth-method.png) + + + + + + You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted, + you will redirect you back to Infisical's App Connections page. ![Azure App Configuration + Authorization](/images/app-connections/azure/grant-access.png) + + + Your **Azure App Configuration Connection** is now available for use. ![Assume Role AWS Connection](/images/app-connections/azure/app-configuration/oauth-connection.png) + + diff --git a/docs/integrations/app-connections/azure-key-vault.mdx b/docs/integrations/app-connections/azure-key-vault.mdx new file mode 100644 index 000000000..f73dab834 --- /dev/null +++ b/docs/integrations/app-connections/azure-key-vault.mdx @@ -0,0 +1,89 @@ +--- +title: "Azure Key Vault Connection" +description: "Learn how to configure a Azure Key Vault Connection for Infisical." +--- + +Infisical currently only supports one method for connecting to Azure, which is OAuth. + + + Using the Azure Key Vault connection on a self-hosted instance of Infisical requires configuring an application in Azure + and registering your instance with it. + + **Prerequisites:** + + - Set up Azure and have an existing Key Vault instance. + + + + Navigate to Azure Active Directory > App registrations to create a new application. + + + Azure Active Directory is now Microsoft Entra ID. + + ![Azure key vault](/images/integrations/azure-app-configuration/config-aad.png) + ![Azure key vault](/images/integrations/azure-app-configuration/config-new-app.png) + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/azure/oauth/callback`. + + The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance. + + + ![Azure key vault](/images/app-connections/azure/register-callback.png) + + + + For the Azure Connection to work with Key Vault, you need to assign multiple permissions to the application. + + #### Azure Key Vault permissions + + Set the API permissions of the Azure application to include `user.impersonation` for the Key Vault API. + ![Azure key vault](/images/app-connections/azure/keyvault-azure-permissions.png) + + + + Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. + + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-1.png) + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-2.png) + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-3.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. + + - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + + Once added, restart your Infisical instance and use the Azure Key Vault connection. + + + + + +## Setup Azure Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/key-vault/select-connection.png) + + + You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID. + + Now select the **OAuth** method and click **Connect to Azure**. + + ![Connect via Azure OAUth](/images/app-connections/azure/key-vault/create-oauth-method.png) + + + + + + You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted, + you will redirect you back to Infisical's App Connections page. ![Azure Key Vault + Authorization](/images/app-connections/azure/grant-access.png) + + + Your **Azure Key Vault Connection** is now available for use. ![Assume Role AWS Connection](/images/app-connections/azure/key-vault/oauth-connection.png) + + diff --git a/docs/integrations/app-connections/databricks.mdx b/docs/integrations/app-connections/databricks.mdx new file mode 100644 index 000000000..38d125ea6 --- /dev/null +++ b/docs/integrations/app-connections/databricks.mdx @@ -0,0 +1,64 @@ +--- +title: "Databricks Connection" +description: "Learn how to configure a Databricks Connection for Infisical." +--- + +Infisical supports the use of [service principals](https://docs.databricks.com/en/admin/users-groups/service-principals.html) to connect with your Databricks workspaces. + +## Configure a Service Principal for Infisical + + + + Navigate to your Databricks Workspace **Settings** via the dropdown in the top right. + ![Workspace Settings Page](/images/app-connections/databricks/workspace-settings.png) + + + Under the **Identity & Access** tab, click the **Manage** button in the **Service Principals** section. + + ![Manage Service Principals](/images/app-connections/databricks/manage-service-principals.png) + + + Click the **Add Service Principal** button. + + ![Add Service Principal](/images/app-connections/databricks/add-service-principal.png) + + + Select the **Add New** option and create a service principal for Infisical. + + ![Create Service Principal](/images/app-connections/databricks/create-service-principal.png) + + + Click on your new service principal, select the **Secrets** tab and click the **Generate Secret** button. + + ![Generate Secret](/images/app-connections/databricks/service-principal-secrets.png) + + + Copy your service principal **Secret** and **Client ID** for use in the following steps. + + ![Generate Secret](/images/app-connections/databricks/service-principal-ids.png) + + + +## Setup Databricks Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Databricks Connection** option from the connection options modal. + ![Select Databricks + Connection](/images/app-connections/databricks/select-databricks-connection.png) + + + Select the **Service Principal** method, add your **workspace URL** and **service principal credentials**, then click **Connect to + Databricks**. ![Connect via Databricks + service principal](/images/app-connections/databricks/create-databricks-service-principal-method.png) + + + Your **Databricks Connection** is now available for use. ![Databricks Service Principal + Connection](/images/app-connections/databricks/databricks-service-principal-connection.png) + + diff --git a/docs/integrations/app-connections/gcp.mdx b/docs/integrations/app-connections/gcp.mdx index d75d2ae12..129c26c2a 100644 --- a/docs/integrations/app-connections/gcp.mdx +++ b/docs/integrations/app-connections/gcp.mdx @@ -81,7 +81,7 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/ ## Setup GCP Connection in Infisical - + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/app-connections/github.mdx b/docs/integrations/app-connections/github.mdx index 1b97d3162..2f8caffed 100644 --- a/docs/integrations/app-connections/github.mdx +++ b/docs/integrations/app-connections/github.mdx @@ -69,7 +69,7 @@ Infisical supports two methods for connecting to GitHub. ## Setup GitHub Connection in Infisical - + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections Tab](/images/app-connections/general/add-connection.png) @@ -135,7 +135,7 @@ Infisical supports two methods for connecting to GitHub. ## Setup GitHub Connection in Infisical - + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections Tab](/images/app-connections/general/add-connection.png) diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 3046c7f47..4b41094b4 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -39,7 +39,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict. - - **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict. + - **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict. - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. 6. Configure the **Details** of your Parameter Store Sync, then click **Next**. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx new file mode 100644 index 000000000..50fa75828 --- /dev/null +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -0,0 +1,142 @@ +--- +title: "AWS Secrets Manager Sync" +description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **AWS Secrets Manager** option. + ![Select AWS Secrets Manager](/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png) + + - **AWS Connection**: The AWS Connection to authenticate with. + - **Region**: The AWS region to deploy secrets to. + - **Mapping Behavior**: Specify how Infisical should map secrets to AWS Secrets Manager: + - **One-To-One**: Each Infisical secret will be mapped to a separate AWS Secrets Manager secret. + - **Many-To-One**: All Infisical secrets will be mapped to a single AWS Secrets Manager secret. + - **Secret Name**: Specifies the name of the AWS Secret to map secrets to if **Many-To-One** mapping behavior is selected. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. + - **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your Secrets Manager Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Secrets Manager Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png) + + 8. If enabled, your Secrets Manager Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png) + + + + To create an **AWS Secrets Manager Sync**, make an API request to the [Create AWS + Secrets Manager Sync](/api-reference/endpoints/secret-syncs/aws-secrets-manager/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/aws-secrets-manager \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-secrets-manager-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "region": "us-east-1", + "mappingBehavior": "one-to-one" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-secrets-manager-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "aws", + "name": "my-aws-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "aws-secrets-manager", + "destinationConfig": { + "region": "us-east-1", + "mappingBehavior": "one-to-one" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx new file mode 100644 index 000000000..3b443c672 --- /dev/null +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -0,0 +1,147 @@ +--- +title: "Azure App Configuration Sync" +description: "Learn how to configure an Azure App Configuration Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure App Configuration. + + + The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal + for Infisical to sync secrets to Azure App Configuration: `Read Key-Value`, `Write Key-Value`, `Delete Key-Value`. + + Any role with these permissions would work such as the **App Configuration Data Owner** role. Alternatively, you can use the **App Configuration Data Contributor** role for read/write access. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Azure App Configuration** option. + ![Select Azure App Configuration](/images/secret-syncs/azure-app-configuration/select-app-config.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/azure-app-configuration/app-config-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/azure-app-configuration/app-config-destination.png) + + - **Azure Connection**: The Azure Connection to authenticate with. + - **Configuration URL**: The URL of your Azure App Configuration. + - **Label**: An optional label to attach to all secrets created by Infisical inside your Azure App Configuration. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/azure-app-configuration/app-config-options.png) + + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. + - **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your Azure App Configuration Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/azure-app-configuration/app-config-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Azure App Configuration Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/azure-app-configuration/app-config-review.png) + + 8. If enabled, your Azure App Configuration Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/azure-app-configuration/app-config-synced.png) + + + + To create an **Azure App Configuration Sync**, make an API request to the [Create Azure App Configuration Sync](/api-reference/endpoints/secret-syncs/azure-app-configuration/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/azure-app-configuration \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-azure-app-configuration-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "configurationUrl": "https://my-azure-app-configuration.azconfig.io", + "label": "my-label" + } + }' + ``` + + ### Sample response + + ```json Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-azure-app-configuration-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "azure", + "name": "my-azure-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "azure-app-configuration", + "destinationConfig": { + "configurationUrl": "https://my-azure-app-configuration.azconfig.io", + "label": "my-label" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx new file mode 100644 index 000000000..9b28f841f --- /dev/null +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -0,0 +1,147 @@ +--- +title: "Azure Key Vault Sync" +description: "Learn how to configure a Azure Key Vault Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure Key Vault. + + + The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal + for Infisical to sync secrets to Azure Key Vault: `secrets/list`, `secrets/get`, `secrets/set`, `secrets/recover`. + + Any role with these permissions would work such as the **Key Vault Secrets Officer** role. + + + + Secrets in Infisical that contain an underscore (`_`) will be converted to a hyphen (`-`) when synced to Azure Key Vault. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Azure Key Vault** option. + ![Select Key Vault](/images/secret-syncs/azure-key-vault/select-key-vault-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/azure-key-vault/vault-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/azure-key-vault/vault-destination.png) + + - **Azure Connection**: The Azure Connection to authenticate with. + - **Vault Base URL**: The URL of your Azure Key Vault. +

+ + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/azure-key-vault/vault-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. + - **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your Azure Key Vault Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/azure-key-vault/vault-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Azure Key Vault Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/azure-key-vault/vault-review.png) + + 8. If enabled, your Azure Key Vault Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/azure-key-vault/vault-synced.png) + + + + To create a **Azure Key Vault Sync**, make an API request to the [Create Key Vault Sync](/api-reference/endpoints/secret-syncs/azure-key-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/azure-key-vault \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-key-vault-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "vaultBaseUrl": "https://my-key-vault.vault.azure.net" + } + }' + ``` + + ### Sample response + + ```json Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-key-vault-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "azure", + "name": "my-azure-key-vault-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "azure-key-vault", + "destinationConfig": { + "vaultBaseUrl": "https://my-key-vault.vault.azure.net" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx new file mode 100644 index 000000000..8f305ecd6 --- /dev/null +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -0,0 +1,140 @@ +--- +title: "Databricks Sync" +description: "Learn how to configure a Databricks Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Databricks Connection](/integrations/app-connections/databricks) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Databricks** option. + ![Select Databricks](/images/secret-syncs/databricks/select-databricks-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/databricks/databricks-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/databricks/databricks-destination.png) + + - **Databricks Connection**: The Databricks Connection to authenticate with. + - **Scope**: The Databricks secret scope to sync secrets to. + + + You must create a secret scope in your Databricks workspace prior to configuration. Ensure your service principal has [Write permissions](https://docs.databricks.com/en/security/auth/access-control/index.html#secret-acls) for the specified secret scope. + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/databricks/databricks-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Databricks does not support importing secrets. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your Databricks Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/databricks/databricks-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Databricks Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/databricks/databricks-review.png) + + 8. If enabled, your Databricks Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/databricks/databricks-created.png) + + + + To create an **Databricks Sync**, make an API request to the [Create Databricks Sync](/api-reference/endpoints/secret-syncs/databricks/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/databricks \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-databricks-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "my-scope" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-databricks-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "databricks", + "name": "my-databricks-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "databricks", + "destinationConfig": { + "scope": "my-scope" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 4c6d52efb..d7d7afd86 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -13,7 +13,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) - 2. Select the **GitHub Store** option. + 2. Select the **GitHub** option. ![Select GitHub](/images/secret-syncs/github/select-github-option.png) 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. diff --git a/docs/internals/components.mdx b/docs/internals/components.mdx index 65506a500..bad4dc076 100644 --- a/docs/internals/components.mdx +++ b/docs/internals/components.mdx @@ -1,28 +1,34 @@ --- title: "Components" -description: "Infisical's components span multiple clients, an API, and a storage backend." +description: "Understand Infisical's core architectural components and how they work together." --- -## Infisical API +## Overview -The Infisical API (sometimes referred to as the **backend**) contains the core platform logic. +Infisical is architected around several key components that work in concert to provide a secure and streamlined secret management experience. These components span the client, API, and storage layers, ensuring that secrets are protected at every stage of their lifecycle. -## Storage backend +## 1. API (Backend) -Infisical relies on a storage backend to store data including users and secrets. Infisical's storage backend is Postgres. +Infisical exposes a well-documented [REST API](https://infisical.com/docs/api-reference/overview/introduction) that enables programmatic interaction with the platform, enabling a wide range of use cases. -## Redis +## 2. Storage Backend -Infisical uses [Redis](https://redis.com) to enable more complex workflows including a queuing system to manage long running asynchronous tasks, cron jobs, as well as reliable cache for frequently used resources. +Infisical relies on a robust storage backend to durably store secrets, users, and other platform data. Infisical's storage backend is [PostgreSQL](https://www.postgresql.org/). -## Infisical Web UI +## 3. Caching Layer -The Web UI is the browser-based portal that connects to the Infisical API. +Infisical uses [Redis](https://redis.com) to enable more complex workflows including a queuing system to manage long-running asynchronous tasks, cron jobs, as well as reliable cache for frequently used resources. -## Infisical clients +## 4. Clients -Clients are any application or infrastructure that connecting to the Infisical API using one of the below methods: -- Public API: Making API requests directly to the Infisical API. -- Client SDK: A platform-specific library with method abstractions for working with secrets. Currently, there are three official SDKs: [Node SDK](https://infisical.com/docs/sdks/languages/node), [Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java). -- CLI: A terminal-based interface for interacting with the Infisical API. -- Kubernetes Operator: This operator retrieves secrets from Infisical and securely store +Clients are interfaces through which users and applications interact with the Infisical API: + +- **Web UI**: A browser-based portal providing a user-friendly interface for managing secrets, configurations, and performing administrative tasks. + +- [**CLI**](https://infisical.com/docs/cli): A terminal-based tool for interacting with the Infisical API, enabling automation, scripting, and integration into CI/CD pipelines. + +- **SDKs (Software Development Kits)**: Platform-specific libraries with method abstractions for working with secrets. Supported languages include [Node.js](https://infisical.com/docs/sdks/languages/node), [Python](https://infisical.com/docs/sdks/languages/python), [Java](https://infisical.com/docs/sdks/languages/java), [Golang](https://infisical.com/docs/sdks/languages/go), [Ruby](https://infisical.com/docs/sdks/languages/ruby) and [.NET](https://infisical.com/docs/sdks/languages/csharp). + +- [**Kubernetes Operator**](https://infisical.com/docs/integrations/platforms/kubernetes): A Kubernetes-native component that facilitates the secure retrieval and management of secrets within a Kubernetes cluster. The operator supports multiple custom resource definitions (CRDs) for syncing secrets. + +- [**Infisical Agent**](https://infisical.com/docs/integrations/platforms/infisical-agent): Daemon that automatically fetches and manages access tokens and secrets to be used in various client resources. diff --git a/docs/mint.json b/docs/mint.json index 176a562b1..b62af6533 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -285,7 +285,7 @@ "pages": [ "self-hosting/overview", { - "group": "Containerized installation methods", + "group": "Installation methods", "pages": [ "self-hosting/deployment-options/standalone-infisical", "self-hosting/deployment-options/docker-swarm", @@ -293,12 +293,12 @@ "self-hosting/deployment-options/kubernetes-helm" ] }, + "self-hosting/guides/upgrading-infisical", "self-hosting/configuration/envars", "self-hosting/configuration/requirements", { "group": "Guides", "pages": [ - "self-hosting/configuration/schema-migrations", "self-hosting/guides/mongo-to-postgres", "self-hosting/guides/custom-certificates" ] @@ -392,8 +392,11 @@ "group": "Connections", "pages": [ "integrations/app-connections/aws", - "integrations/app-connections/github", - "integrations/app-connections/gcp" + "integrations/app-connections/azure-app-configuration", + "integrations/app-connections/azure-key-vault", + "integrations/app-connections/databricks", + "integrations/app-connections/gcp", + "integrations/app-connections/github" ] } ] @@ -406,8 +409,12 @@ "group": "Syncs", "pages": [ "integrations/secret-syncs/aws-parameter-store", - "integrations/secret-syncs/github", - "integrations/secret-syncs/gcp-secret-manager" + "integrations/secret-syncs/aws-secrets-manager", + "integrations/secret-syncs/azure-app-configuration", + "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/databricks", + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/github" ] } ] @@ -820,15 +827,39 @@ ] }, { - "group": "GitHub", + "group": "Azure App Configuration", "pages": [ - "api-reference/endpoints/app-connections/github/list", - "api-reference/endpoints/app-connections/github/available", - "api-reference/endpoints/app-connections/github/get-by-id", - "api-reference/endpoints/app-connections/github/get-by-name", - "api-reference/endpoints/app-connections/github/create", - "api-reference/endpoints/app-connections/github/update", - "api-reference/endpoints/app-connections/github/delete" + "api-reference/endpoints/app-connections/azure-app-configuration/list", + "api-reference/endpoints/app-connections/azure-app-configuration/available", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-id", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-name", + "api-reference/endpoints/app-connections/azure-app-configuration/create", + "api-reference/endpoints/app-connections/azure-app-configuration/update", + "api-reference/endpoints/app-connections/azure-app-configuration/delete" + ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/app-connections/azure-key-vault/list", + "api-reference/endpoints/app-connections/azure-key-vault/available", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-id", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-name", + "api-reference/endpoints/app-connections/azure-key-vault/create", + "api-reference/endpoints/app-connections/azure-key-vault/update", + "api-reference/endpoints/app-connections/azure-key-vault/delete" + ] + }, + { + "group": "Databricks", + "pages": [ + "api-reference/endpoints/app-connections/databricks/list", + "api-reference/endpoints/app-connections/databricks/available", + "api-reference/endpoints/app-connections/databricks/get-by-id", + "api-reference/endpoints/app-connections/databricks/get-by-name", + "api-reference/endpoints/app-connections/databricks/create", + "api-reference/endpoints/app-connections/databricks/update", + "api-reference/endpoints/app-connections/databricks/delete" ] }, { @@ -842,6 +873,18 @@ "api-reference/endpoints/app-connections/gcp/update", "api-reference/endpoints/app-connections/gcp/delete" ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/app-connections/github/list", + "api-reference/endpoints/app-connections/github/available", + "api-reference/endpoints/app-connections/github/get-by-id", + "api-reference/endpoints/app-connections/github/get-by-name", + "api-reference/endpoints/app-connections/github/create", + "api-reference/endpoints/app-connections/github/update", + "api-reference/endpoints/app-connections/github/delete" + ] } ] }, @@ -865,16 +908,58 @@ ] }, { - "group": "GitHub", + "group": "AWS Secrets Manager", "pages": [ - "api-reference/endpoints/secret-syncs/github/list", - "api-reference/endpoints/secret-syncs/github/get-by-id", - "api-reference/endpoints/secret-syncs/github/get-by-name", - "api-reference/endpoints/secret-syncs/github/create", - "api-reference/endpoints/secret-syncs/github/update", - "api-reference/endpoints/secret-syncs/github/delete", - "api-reference/endpoints/secret-syncs/github/sync-secrets", - "api-reference/endpoints/secret-syncs/github/remove-secrets" + "api-reference/endpoints/secret-syncs/aws-secrets-manager/list", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/create", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/update", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/delete", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-app-configuration/list", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name", + "api-reference/endpoints/secret-syncs/azure-app-configuration/create", + "api-reference/endpoints/secret-syncs/azure-app-configuration/update", + "api-reference/endpoints/secret-syncs/azure-app-configuration/delete", + "api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets" + ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-key-vault/list", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name", + "api-reference/endpoints/secret-syncs/azure-key-vault/create", + "api-reference/endpoints/secret-syncs/azure-key-vault/update", + "api-reference/endpoints/secret-syncs/azure-key-vault/delete", + "api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets" + ] + }, + { + "group": "Databricks", + "pages": [ + "api-reference/endpoints/secret-syncs/databricks/list", + "api-reference/endpoints/secret-syncs/databricks/get-by-id", + "api-reference/endpoints/secret-syncs/databricks/get-by-name", + "api-reference/endpoints/secret-syncs/databricks/create", + "api-reference/endpoints/secret-syncs/databricks/update", + "api-reference/endpoints/secret-syncs/databricks/delete", + "api-reference/endpoints/secret-syncs/databricks/sync-secrets", + "api-reference/endpoints/secret-syncs/databricks/remove-secrets" ] }, { @@ -890,6 +975,19 @@ "api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets", "api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets" ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/secret-syncs/github/list", + "api-reference/endpoints/secret-syncs/github/get-by-id", + "api-reference/endpoints/secret-syncs/github/get-by-name", + "api-reference/endpoints/secret-syncs/github/create", + "api-reference/endpoints/secret-syncs/github/update", + "api-reference/endpoints/secret-syncs/github/delete", + "api-reference/endpoints/secret-syncs/github/sync-secrets", + "api-reference/endpoints/secret-syncs/github/remove-secrets" + ] } ] }, @@ -1024,6 +1122,8 @@ "group": "Keys", "pages": [ "api-reference/endpoints/kms/keys/list", + "api-reference/endpoints/kms/keys/get-by-id", + "api-reference/endpoints/kms/keys/get-by-name", "api-reference/endpoints/kms/keys/create", "api-reference/endpoints/kms/keys/update", "api-reference/endpoints/kms/keys/delete", diff --git a/docs/self-hosting/configuration/requirements.mdx b/docs/self-hosting/configuration/requirements.mdx index e0e992b7f..7ed2691f2 100644 --- a/docs/self-hosting/configuration/requirements.mdx +++ b/docs/self-hosting/configuration/requirements.mdx @@ -22,33 +22,34 @@ The actual resource requirements will vary in direct proportion to the operation Infisical doesn’t require file storage as all persisted data is saved in the database. However, its logs and metrics are saved to disk for later viewing. As a result, we recommend provisioning 1-2 GB of storage. -### CPU +### CPU and Memory (Per Container/Instance) -CPU requirements vary heavily on the volume of secret operations (reads and writes) you anticipate. -Processing large volumes of secrets frequently and consistently will require higher CPU. +Infisical is stateless and scales horizontally by running across multiple containers/instances. Each instance typically does **not** need more than **2–4 CPU cores** and **4–8 GB** of memory. +If you need additional capacity, simply increase the **number** of containers/instances running in parallel. -Recommended minimum CPU hardware for different sizes of deployments: +| **Deployment Size** | **CPU (Cores, per container)** | **Memory (GB, per container)** | **Recommended Number of Containers** | +|---------------------|--------------------------------|--------------------------------|--------------------------------------| +| **Small** | 2 | 4 | 2+ | +| **Medium** | 2–4 | 4–8 | 5+ | +| **Large** | 2–4 | 4–8 | 10+ | -- **small:** 2-4 core is the **recommended** minimum -- **large:** 4-8 cores are suitable for larger deployments - -### Memory Allocation - -Memory needs depend on expected workload, including factors like user activity, automation level, and the frequency of secret operations. - -Recommended minimum memory hardware for different sizes of deployments: -- **small:** 4-8 GB is the **recommended** minimum -- **large:** 16-32 GB are suitable for larger deployments +> **Note:** +> - Adding more containers (horizontal scaling) is generally the best way to handle spikes in secret operations. +> - If you prefer, you can increase CPU/memory on a single container (vertical scaling), but horizontal scaling is more flexible and resilient. ## Database & caching layer ### Postgres PostgreSQL is the only database supported by Infisical. Infisical has been extensively tested with Postgres version 16. We recommend using versions 14 and up for optimal compatibility. +The compute required for Postgres is largely dependent on the number of secret operations (reads and writes) you expect. The more frequently you read and write secrets, the more compute you will need. +You'll notice that storage requirements are high and this is because audit logs are by default stored in the database. -Recommended resource allocation based on deployment size: -- **small:** 2 vCPU / 8 GB RAM / 20 GB Disk -- **large:** 4vCPU / 16 GB RAM / 100 GB Disk + +Recommended resource allocation based on deployment size. You may require more resources if you have a large number of secrets or high transaction volume: +- **small:** 2 vCPU / 8 GB RAM / 100 GB Disk +- **medium:** 4vCPU / 16 GB RAM / 200 GB Disk +- **large:** 8vCPU / 32 GB RAM / 500 GB Disk ### Redis diff --git a/docs/self-hosting/configuration/schema-migrations.mdx b/docs/self-hosting/configuration/schema-migrations.mdx deleted file mode 100644 index 5df52e713..000000000 --- a/docs/self-hosting/configuration/schema-migrations.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: "Schema migration" -description: "Learn how to run Postgres schema migrations." ---- - -Running schema migrations is a requirement before deploying Infisical. -Each time you decide to upgrade your version of Infisical, it's necessary to run schema migrations for that specific version. -The guide below outlines a step-by-step guide to help you manually run schema migrations for Infisical. - -### Prerequisites -- Docker installed on your machine -- An active PostgreSQL database -- Postgres database connection string - - - - First, ensure you have the correct version of the Infisical Docker image. You can pull it from Docker Hub using the following command: - ```bash - docker pull infisical/infisical: - ``` - Replace `` with the specific version number you intend to deploy. View available versions [here](https://hub.docker.com/r/infisical/infisical/tags) - - - - The Docker image requires a `DB_CONNECTION_URI` environment variable. This connection string should point to your PostgreSQL database. The format generally looks like this: `postgresql://username:password@host:port/database`. - - - - To run the schema migration for the version of Infisical you want to deploy, use the following Docker command: - - ```bash - docker run --env DB_CONNECTION_URI= infisical/infisical: npm run migration:latest - ``` - Replace `` with your actual PostgreSQL connection string, and `` with the desired version number. - - - - After running the migration, it's good practice to check if the migration was successful. You can do this by checking the logs or accessing your database to ensure the schema has been updated accordingly. - - - If you need to rollback a migration by one step, use the following command: - - ```bash - docker run --env DB_CONNECTION_URI= infisical/infisical: npm run migration:rollback - ``` - - - - It's important to run schema migrations for each version of the Infisical you deploy. For instance, if you're updating from `infisical/infisical:1` to `infisical/infisical:2`, ensure you run the schema migrations for `infisical/infisical:2` before deploying it. - - - - - In a production setting, we recommend a more structured approach to deploying migrations prior to upgrading Infisical. This can be accomplished via CI automation. - - -### Additional discussion -- Always back up your database before running migrations, especially in a production environment. -- Test the migration process in a staging environment before applying it to production. -- Keep track of the versions and their corresponding migrations to avoid any inconsistencies. diff --git a/docs/self-hosting/deployment-options/docker-swarm.mdx b/docs/self-hosting/deployment-options/docker-swarm.mdx index 5fce38b29..c7280d879 100644 --- a/docs/self-hosting/deployment-options/docker-swarm.mdx +++ b/docs/self-hosting/deployment-options/docker-swarm.mdx @@ -157,23 +157,6 @@ The [Docker stack file](https://github.com/Infisical/infisical/tree/main/docker- 3lznscvk7k5t infisical_spolo2 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2 v04ml7rz2j5q infisical_spolo3 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2 ``` - - - You'll notice that service `infisical_infisical` will not be in running state. - This is expected as the database does not yet have the desired schemas. - Once the database schema migrations have been successfully applied, this issue should be resolved. - - - - - Run the schema migration to initialize the database. Follow the [guide here](/self-hosting/configuration/schema-migrations) to learn how. - - To run the migrations, you'll need to connect to the Postgres instance deployed on your Docker swarm. The default Postgres user credentials are defined in the Docker swarm: username: `postgres`, password: `postgres` and database: `postgres`. - We recommend you change these credentials when deploying to production and creating a separate DB for Infisical. - - - After running the schema migrations, be sure to update the `.env` file to have the correct `DB_CONNECTION_URI`. - diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index 35b003c2d..e0e5ba75d 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -78,18 +78,6 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete - - Infisical relies on a relational database, which means that database schemas need to be migrated before the instance can become operational. - - To automate this process, the chart includes a option named `infisical.autoDatabaseSchemaMigration`. - When this option is enabled, a deployment/upgrade will only occur _after_ a successful schema migration. - - - If you are using in-cluster Postgres, you may notice the migration job failing initially. - This is expected as it is waiting for the database to be in ready state. - - - By default, this chart uses Nginx as its Ingress controller to direct traffic to Infisical services. diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx index 6e41e4b5f..b7f76ce98 100644 --- a/docs/self-hosting/deployment-options/standalone-infisical.mdx +++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx @@ -22,11 +22,6 @@ The following guide provides a detailed step-by-step walkthrough on how you can Remember to replace `` with the docker image tag of your choice. - - Before you can start the instance of Infisical, you need to run the database schema migrations. - Follow the step by [step guide here](/self-hosting/configuration/schema-migrations) on running schema migrations for Infisical. - - For a minimal installation of Infisical, you must configure `ENCRYPTION_KEY`, `AUTH_SECRET`, `DB_CONNECTION_URI`, `SITE_URL`, and `REDIS_URL`. [View all available configurations](/self-hosting/configuration/envars). diff --git a/docs/self-hosting/guides/upgrading-infisical.mdx b/docs/self-hosting/guides/upgrading-infisical.mdx new file mode 100644 index 000000000..60c6edbff --- /dev/null +++ b/docs/self-hosting/guides/upgrading-infisical.mdx @@ -0,0 +1,57 @@ +--- + +title: "Upgrade Infisical Instance" +description: "How to upgrade Infisical self-hosted instance" + +--- + +Keeping your Infisical instance up to date is key to making sure you receive the latest performance improvements, security patches, and feature updates. +We release updates approximately once a week, which may include new features, bug fixes, performance enhancements, and critical security patches. + +Since secrets management is a critical component of your infrastructure, we aim to avoid disruptive changes that will impact fetching secrets in downstream clients. +If a release requires specific attention, a note will be attached to the corresponding [release](https://github.com/Infisical/infisical/releases) version. + +During an upgrade, two key components are updated: + +- **Infisical Application:** The core application code is updated. +- **PostgreSQL Database Schema:** Schema migrations run automatically to ensure your database remains in sync with the updated application. + +> **Before You Upgrade:** +> **Always back up your database.** While our automated migration system is robust, having a backup ensures you can recover quickly in the event of an issue. + +## Automated Schema Migrations + +In previous versions (prior to `v0.111.0-postgres`), schema migrations had to be executed manually before starting the application. +Now, migrations run automatically during boot-up. This improvement streamlines the upgrade process, reduces manual steps, and minimizes the risk of inconsistencies between your database schema and application code. + +### Benefits of Automated Migrations + +- **Seamless Integration:** + Migrations are now part of the boot-up process, removing the need for manual intervention. + +- **Synchronous Upgrades:** + In multi-instance deployments, one instance acquires a lock and performs the migration while the others wait. This ensures that if a migration fails, the rollout is halted to prevent inconsistencies. + +- **Reduced Room for Error:** + Automatic migrations help ensure that your database schema always remains in sync with your application code. + +## Upgrade Steps + +1. **Back Up Your Data:** + - Ensure you have a complete backup of your Postgres database. + - Verify that your backup is current and accessible. + +2. **Select the Upgrade Version:** + - Visit the [Infisical releases page](https://github.com/Infisical/infisical/releases) for a list of available versions. + - Look for releases with the prefix `infisical/` as there are other releases that are not related to the Infisical instance. + +3. **Start the Upgrade Process:** + - Launch the new version of Infisical. During startup, the application will automatically compare the current database schema with the updated schema in the code. + - If any differences are detected, Infisical will apply the necessary migrations automatically. + +4. **Multi-Instance Coordination:** + - In environments with multiple instances, one instance will acquire a lock and perform the migration while the other instances wait. + - Once the migration is complete, all instances will operate with the updated schema. + +5. **Verify the Upgrade:** + - Review the logs for any migration errors or warnings. diff --git a/docs/self-hosting/reference-architectures/aws-ecs.mdx b/docs/self-hosting/reference-architectures/aws-ecs.mdx index 5a71c9572..12deaa3cb 100644 --- a/docs/self-hosting/reference-architectures/aws-ecs.mdx +++ b/docs/self-hosting/reference-architectures/aws-ecs.mdx @@ -48,9 +48,3 @@ This ensures that if there is a failure in one availability zone, the working re Yes, Infisical can function in an air-gapped environment. To do so, update your ECS task to use the publicly available AWS Elastic Container Registry (ECR) image instead of the default Docker Hub image. Additionally, it's necessary to configure VPC endpoints, which allows your system to access AWS ECR via a private network route instead of the internet, ensuring all connectivity remains within the secure, private network. - - Since the Amazon RDS instance is housed within a private network to enhance security, it is not directly accessible from the internet. This means that in order to run the required [Postgres schema migrations](/self-hosting/configuration/schema-migrations), you need to connect to this instance of RDS. There are many approaches you can take: - - To automate schema migrations, you may setup CI/CD pipeline with access to the same RDS network to run the schema migrations before making deployment to ECS. This ensures that if migrations fail, your Infisical instances continues to run. - - If you would like to run the migrations manually, consider using AWS Systems Manager Session Manager to access the RDS within the VPC on your local machine. - - If your organization already has mechanisms in place for secure access to the VPC, such as VPNs or Direct Connect, these can also be utilized for performing database migrations manually. - diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index 5c68e440d..8b6af1169 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -222,10 +222,10 @@ "org-members-description": "Manage members of your organization. These users could afterwards be formed into projects.", "search-members": "Search members...", "add-dialog": { - "add-member-to-project": "Add a member to your project", + "add-member-to-project": "Add users to your project", "already-all-invited": "All the users in your organization are already invited.", "add-user-org-first": "Add more users to the organization first.", - "user-will-email": "The user will receive an email with the instructions.", + "user-will-email": "Users will receive an email with instructions to gain access.", "looking-add": "<0>If you are looking to add users to your org,<1>click here", "add-user-to-org": "Add Users to Organization" } diff --git a/frontend/public/lotties/settings-cog.json b/frontend/public/lotties/settings-cog.json new file mode 100644 index 000000000..fdf1333c7 --- /dev/null +++ b/frontend/public/lotties/settings-cog.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":500,"h":500,"nm":"system-regular-63-settings-cog","ddd":0,"assets":[{"id":"comp_1","nm":"hover-cog-1","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.38],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":60,"s":[180]}],"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[65.415,17.27],[65.425,17.27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-5.279,-1.58],[-4.338,0],[-4.502,1.145],[-2.995,1.872],[-0.218,0.122],[-4.41,7.876],[-0.134,0.213],[-1.581,5.285],[0,4.342],[1.146,4.497],[1.868,2.989],[0.123,0.22],[7.877,4.411],[0.214,0.133],[5.283,1.581],[4.342,0],[4.498,-1.145],[2.992,-1.871],[0.22,-0.123],[4.411,-7.878],[0.134,-0.213],[1.581,-5.283],[0,-4.341],[-1.144,-4.497],[-1.871,-2.993],[-0.123,-0.219],[-7.879,-4.413],[-0.213,-0.132]],"o":[[4.502,1.145],[4.338,0],[5.28,-1.581],[0.212,-0.132],[7.878,-4.413],[0.123,-0.22],[1.868,-2.99],[1.146,-4.495],[0,-4.342],[-1.581,-5.285],[-0.134,-0.214],[-4.411,-7.877],[-0.219,-0.123],[-2.991,-1.87],[-4.497,-1.145],[-4.341,0],[-5.282,1.581],[-0.214,0.133],[-7.876,4.41],[-0.123,0.219],[-1.87,2.992],[-1.145,4.498],[0,4.341],[1.581,5.284],[0.133,0.213],[4.41,7.876],[0.218,0.122],[2.996,1.872]],"v":[[-13.321,50.103],[-0.001,51.829],[13.318,50.104],[25.456,45.04],[26.102,44.658],[44.885,25.874],[45.271,25.224],[50.331,13.088],[52.057,-0.229],[50.331,-13.547],[45.271,-25.682],[44.885,-26.332],[26.103,-45.116],[25.453,-45.5],[13.316,-50.562],[-0.001,-52.287],[-13.319,-50.562],[-25.455,-45.5],[-26.105,-45.115],[-44.888,-26.332],[-45.272,-25.684],[-50.334,-13.548],[-52.059,-0.229],[-50.335,13.087],[-45.271,25.226],[-44.888,25.874],[-26.104,44.658],[-25.458,45.04]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[7.066,0],[7.15,1.852],[0.174,0.051],[7.142,4.396],[7.169,12.665],[1.507,5.085],[0.045,0.174],[0,7.071],[-1.853,7.147],[-0.051,0.173],[-4.395,7.138],[-12.665,7.167],[-5.085,1.507],[-0.174,0.045],[-7.071,0],[-7.146,-1.853],[-0.172,-0.051],[-7.138,-4.395],[-7.169,-12.666],[-1.508,-5.086],[-0.045,-0.174],[0,-7.073],[1.854,-7.145],[0.051,-0.171],[4.395,-7.136],[12.666,-7.17],[5.083,-1.507],[0.176,-0.045]],"o":[[-7.065,0],[-0.175,-0.046],[-5.082,-1.506],[-12.667,-7.17],[-4.395,-7.139],[-0.051,-0.172],[-1.853,-7.146],[0,-7.071],[0.045,-0.175],[1.508,-5.085],[7.17,-12.666],[7.138,-4.396],[0.172,-0.051],[7.147,-1.853],[7.072,0],[0.174,0.045],[5.086,1.508],[12.664,7.168],[4.394,7.134],[0.051,0.172],[1.854,7.146],[0,7.074],[-0.045,0.173],[-1.508,5.086],[-7.168,12.664],[-7.142,4.396],[-0.174,0.051],[-7.149,1.852]],"v":[[-0.001,83.129],[-21.426,80.338],[-21.949,80.193],[-41.716,71.788],[-72.018,41.488],[-80.423,21.717],[-80.567,21.197],[-83.359,-0.229],[-80.567,-21.656],[-80.423,-22.178],[-72.018,-41.946],[-41.719,-72.244],[-21.949,-80.651],[-21.43,-80.795],[-0.001,-83.587],[21.427,-80.795],[21.946,-80.651],[41.717,-72.244],[72.015,-41.945],[80.42,-22.178],[80.563,-21.659],[83.357,-0.229],[80.563,21.2],[80.42,21.717],[72.015,41.487],[41.716,71.787],[21.946,80.193],[21.423,80.338]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[-0.007,0.004],[0,0]],"o":[[0,0],[0.006,-0.003]],"v":[[142.26,-118.378],[142.24,-118.367]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0,0],[-0.007,-0.004]],"o":[[0.007,0.004],[0,0]],"v":[[-142.23,-118.359],[-142.211,-118.348]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[173.794,64.156],[173.804,64.156]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[0.007,0.003],[0,0]],"o":[[0,0],[-0.006,-0.003]],"v":[[142.209,117.89],[142.228,117.901]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ind":7,"ty":"sh","ix":8,"ks":{"a":0,"k":{"i":[[0,0],[0.006,-0.003]],"o":[[-0.007,0.003],[0,0]],"v":[[-142.24,117.907],[-142.259,117.918]],"c":true},"ix":2},"nm":"Path 8","mn":"ADBE Vector Shape - Group","hd":false},{"ind":8,"ty":"sh","ix":9,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-6.556,2.028],[-14.196,13.044],[-5.925,-3.438],[0,0],[0,0],[0,0],[-1.534,6.696],[0,9.907],[1.949,8.508],[-5.948,3.436],[0,0],[0,0],[0,0],[5.044,4.635],[17.51,5.416],[0,6.862],[0,0],[0,0],[0,0],[6.556,-2.027],[14.196,-13.046],[5.926,3.438],[0,0],[0,0],[0,0],[1.535,-6.697],[0,-9.912],[-1.949,-8.505],[5.949,-3.437],[0,0],[0,0],[0,0],[-5.046,-4.636],[-17.514,-5.417],[0,-6.862]],"o":[[0,0],[0,0],[0,-6.862],[17.513,-5.417],[5.045,-4.635],[0,0],[0,0],[0,0],[-5.948,-3.437],[1.949,-8.51],[0,-9.908],[-1.534,-6.696],[0,0],[0,0],[0,0],[-5.927,3.438],[-14.198,-13.046],[-6.556,-2.027],[0,0],[0,0],[0,0],[0,6.862],[-17.511,5.416],[-5.046,4.636],[0,0],[0,0],[0,0],[5.949,3.437],[-1.948,8.503],[0,9.911],[1.534,6.697],[0,0],[0,0],[0,0],[5.927,-3.439],[14.194,13.044],[6.556,2.028],[0,0]],"v":[[-31.226,176.828],[31.224,176.828],[31.224,143.104],[42.249,128.152],[90.035,100.33],[108.479,98.318],[137.809,115.337],[168.973,61.378],[139.879,44.572],[132.452,27.526],[135.39,-0.229],[132.452,-27.984],[139.879,-45.03],[168.973,-61.837],[137.809,-115.795],[108.479,-98.776],[90.035,-100.788],[42.25,-128.611],[31.224,-143.562],[31.224,-177.286],[-31.226,-177.286],[-31.226,-143.562],[-42.252,-128.611],[-90.036,-100.788],[-108.48,-98.776],[-137.812,-115.795],[-168.976,-61.836],[-139.881,-45.031],[-132.455,-27.982],[-135.392,-0.229],[-132.454,27.525],[-139.881,44.572],[-168.976,61.378],[-137.812,115.337],[-108.48,98.318],[-90.036,100.33],[-42.251,128.152],[-31.226,143.104]],"c":true},"ix":2},"nm":"Path 9","mn":"ADBE Vector Shape - Group","hd":false},{"ind":9,"ty":"sh","ix":10,"ks":{"a":0,"k":{"i":[[14.373,0],[0,0],[0,14.373],[0,0],[12.287,9.814],[0,0],[7.26,12.444],[0,0],[0.067,0.125],[-12.372,6.865],[0,0],[0,8.014],[-1.062,7.435],[0,0],[-6.843,12.634],[-0.072,0.124],[0,0],[-6.784,1.764],[-6.019,-3.511],[0,0],[-14.219,5.629],[0,0],[-14.373,0],[0,0],[0,-14.373],[0,0],[-12.288,-9.814],[0,0],[-7.261,-12.448],[0,0],[-0.068,-0.125],[12.369,-6.866],[0,0],[0,-8.012],[1.062,-7.437],[0,0],[6.844,-12.634],[0.071,-0.124],[0,0],[12.468,7.269],[0,0],[14.221,-5.63],[0,0]],"o":[[0,0],[-14.373,0],[0,0],[-14.221,-5.63],[0,0],[-12.438,7.253],[0,0],[-0.071,-0.124],[-6.844,-12.634],[0,0],[-1.062,-7.436],[0,-8.014],[0,0],[-12.371,-6.865],[0.068,-0.125],[0,0],[3.559,-6.101],[6.745,-1.756],[0,0],[12.287,-9.814],[0,0],[0,-14.373],[0,0],[14.373,0],[0,0],[14.218,5.629],[0,0],[12.436,-7.256],[0,0],[0.071,0.124],[6.842,12.633],[0,0],[1.062,7.437],[0,8.011],[0,0],[12.372,6.865],[-0.067,0.125],[0,0],[-7.293,12.502],[0,0],[-12.288,9.814],[0,0],[0,14.373]],"v":[[36.457,208.128],[-36.459,208.128],[-62.526,182.062],[-62.526,154.174],[-102.371,130.96],[-126.521,144.973],[-162.269,135.573],[-198.761,72.389],[-198.97,72.015],[-188.935,36.76],[-165.094,22.989],[-166.692,-0.229],[-165.094,-23.448],[-188.936,-37.219],[-198.971,-72.473],[-198.761,-72.848],[-162.303,-135.973],[-146.283,-148.136],[-126.489,-145.413],[-102.371,-131.418],[-62.526,-154.633],[-62.526,-182.52],[-36.459,-208.586],[36.457,-208.586],[62.524,-182.52],[62.524,-154.633],[102.369,-131.418],[126.519,-145.432],[162.266,-136.032],[198.759,-72.848],[198.969,-72.473],[188.935,-37.221],[165.092,-23.448],[166.69,-0.229],[165.092,22.989],[188.934,36.761],[198.968,72.015],[198.759,72.389],[162.3,135.514],[126.49,144.957],[102.369,130.96],[62.524,154.174],[62.524,182.062]],"c":true},"ix":2},"nm":"Path 10","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-63-settings-cog').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":11,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":425,"st":60,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[0.914,0.91,0.91],"ix":1}}]}],"ip":0,"op":271,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-cog-1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-cog-1","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/src/components/permissions/ProjectPermissionCan.tsx b/frontend/src/components/permissions/ProjectPermissionCan.tsx index d8f794bf9..f49709ff3 100644 --- a/frontend/src/components/permissions/ProjectPermissionCan.tsx +++ b/frontend/src/components/permissions/ProjectPermissionCan.tsx @@ -57,6 +57,10 @@ export const ProjectPermissionCan: FunctionComponent const finalChild = typeof children === "function" ? children(isAllowed, ability as any) : children; + if (!isAllowed && renderGuardBanner) { + return ; + } + if (!isAllowed && passThrough) { return {finalChild}; } @@ -65,10 +69,6 @@ export const ProjectPermissionCan: FunctionComponent return {finalChild}; } - if (!isAllowed && renderGuardBanner) { - return ; - } - if (!isAllowed) return null; return finalChild; diff --git a/frontend/src/components/secret-syncs/DeleteSecretSyncModal.tsx b/frontend/src/components/secret-syncs/DeleteSecretSyncModal.tsx index 1daed67a3..71c20832b 100644 --- a/frontend/src/components/secret-syncs/DeleteSecretSyncModal.tsx +++ b/frontend/src/components/secret-syncs/DeleteSecretSyncModal.tsx @@ -18,7 +18,7 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp if (!secretSync) return null; - const { id: syncId, name, destination } = secretSync; + const { id: syncId, name, destination, projectId } = secretSync; const handleDeleteSecretSync = async () => { const destinationName = SECRET_SYNC_MAP[destination].name; @@ -27,7 +27,8 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp await deleteSync.mutateAsync({ syncId, destination, - removeSecrets + removeSecrets, + projectId }); createNotification({ diff --git a/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx b/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx index 8f0fdc40e..8b587c62f 100644 --- a/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx @@ -37,7 +37,7 @@ const FormSchema = z.object({ type TFormData = z.infer; const Content = ({ secretSync, onComplete }: ContentProps) => { - const { id: syncId, destination } = secretSync; + const { id: syncId, destination, projectId } = secretSync; const destinationName = SECRET_SYNC_MAP[destination].name; const { @@ -53,7 +53,8 @@ const Content = ({ secretSync, onComplete }: ContentProps) => { await triggerImportSecrets.mutateAsync({ syncId, destination, - importBehavior + importBehavior, + projectId }); createNotification({ diff --git a/frontend/src/components/secret-syncs/SecretSyncRemoveSecretsModal.tsx b/frontend/src/components/secret-syncs/SecretSyncRemoveSecretsModal.tsx index c8bdeee6d..718392b92 100644 --- a/frontend/src/components/secret-syncs/SecretSyncRemoveSecretsModal.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncRemoveSecretsModal.tsx @@ -15,7 +15,7 @@ type ContentProps = { }; const Content = ({ secretSync, onComplete }: ContentProps) => { - const { id: syncId, destination } = secretSync; + const { id: syncId, destination, projectId } = secretSync; const destinationName = SECRET_SYNC_MAP[destination].name; const triggerSyncImport = useTriggerSecretSyncRemoveSecrets(); @@ -24,7 +24,8 @@ const Content = ({ secretSync, onComplete }: ContentProps) => { try { await triggerSyncImport.mutateAsync({ syncId, - destination + destination, + projectId }); createNotification({ diff --git a/frontend/src/components/secret-syncs/SecretSyncSelect.tsx b/frontend/src/components/secret-syncs/SecretSyncSelect.tsx index 6e3030a01..cbcba4513 100644 --- a/frontend/src/components/secret-syncs/SecretSyncSelect.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncSelect.tsx @@ -10,9 +10,9 @@ type Props = { }; export const SecretSyncSelect = ({ onSelect }: Props) => { - const { isLoading, data: secretSyncOptions } = useSecretSyncOptions(); + const { isPending, data: secretSyncOptions } = useSecretSyncOptions(); - if (isLoading) { + if (isPending) { return (

diff --git a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx index d9306c671..6d15e8007 100644 --- a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx @@ -41,7 +41,8 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => syncId: secretSync.id, ...formData, environment: environment?.slug, - connectionId: connection.id + connectionId: connection.id, + projectId: secretSync.projectId }); createNotification({ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx index 408e4af67..94e587709 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx @@ -23,7 +23,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { const destination = watch("destination"); const app = SECRET_SYNC_CONNECTION_MAP[destination]; - const { data: options, isLoading } = useListAvailableAppConnections(app); + const { data: availableConnections, isPending } = useListAvailableAppConnections(app); const connectionName = APP_CONNECTION_MAP[app].name; @@ -54,8 +54,8 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { onChange(newValue); if (callback) callback(); }} - isLoading={isLoading} - options={options} + isLoading={isPending} + options={availableConnections} placeholder="Select connection..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} @@ -65,18 +65,14 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { control={control} name="connection" /> - {options?.length === 0 && ( + {availableConnections?.length === 0 && (

{canCreateConnection ? ( <> You do not have access to any {appName} Connections. Create one from the{" "} - - Organization Settings + + App Connections {" "} page. diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx index a77b0cc04..09076ea9c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsParameterStoreSyncFields.tsx @@ -1,30 +1,11 @@ import { Controller, useFormContext } from "react-hook-form"; -import { components, OptionProps, SingleValue } from "react-select"; -import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; -import { Badge, FilterableSelect, FormControl, Input } from "@app/components/v2"; -import { AWS_REGIONS } from "@app/helpers/appConnections"; +import { FormControl, Input } from "@app/components/v2"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; - -const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => { - return ( - -

-

{children}

- - {props.data.slug} - - {isSelected && ( - - )} -
- - ); -}; +import { AwsRegionSelect } from "./shared"; export const AwsParameterStoreSyncFields = () => { const { control } = useFormContext< @@ -37,17 +18,7 @@ export const AwsParameterStoreSyncFields = () => { ( - region.slug === value)} - onChange={(option) => - onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug) - } - options={AWS_REGIONS} - placeholder="Select region..." - getOptionLabel={(option) => option.name} - getOptionValue={(option) => option.slug} - components={{ Option }} - /> + )} control={control} diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsSecretsManagerSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsSecretsManagerSyncFields.tsx new file mode 100644 index 000000000..d971f4899 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AwsSecretsManagerSyncFields.tsx @@ -0,0 +1,96 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FormControl, Input, Select, SelectItem } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +import { TSecretSyncForm } from "../schemas"; +import { AwsRegionSelect } from "./shared"; + +export const AwsSecretsManagerSyncFields = () => { + const { control, watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AWSSecretsManager } + >(); + + const mappingBehavior = watch("destinationConfig.mappingBehavior"); + + return ( + <> + + ( + + + + )} + control={control} + name="destinationConfig.region" + /> + ( + +

Specify how Infisical should map secrets to AWS Secrets Manager:

+
    +
  • +

    + One-To-One: Each + Infisical secret will be mapped to a separate AWS Secrets Manager secret. +

    +
  • +
  • +

    + Many-To-One: All + Infisical secrets will be mapped to a single AWS Secrets Manager secret. +

    +
  • +
+
+ } + errorText={error?.message} + isError={Boolean(error?.message)} + label="Mapping Behavior" + > + + + )} + /> + {mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && ( + ( + + + + )} + control={control} + name="destinationConfig.secretName" + /> + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx new file mode 100644 index 000000000..b4d1f8c42 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx @@ -0,0 +1,53 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const AzureAppConfigurationSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration } + >(); + + return ( + <> + { + setValue("destinationConfig.configurationUrl", ""); + }} + /> + ( + + + + )} + /> + + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureKeyVaultSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureKeyVaultSyncFields.tsx new file mode 100644 index 000000000..a5316d81e --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureKeyVaultSyncFields.tsx @@ -0,0 +1,47 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const AzureKeyVaultSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AzureKeyVault } + >(); + + return ( + <> + { + setValue("destinationConfig.vaultBaseUrl", ""); + }} + /> + ( + + + + )} + /> + +
+ +

+ Secret keys with underscores (_) will be converted to hyphens (-) when syncing to Azure + Key Vault. +

+
+ + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx new file mode 100644 index 000000000..c6cfff0e5 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx @@ -0,0 +1,72 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { + TDatabricksSecretScope, + useDatabricksConnectionListSecretScopes +} from "@app/hooks/api/appConnections/databricks"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const DatabricksSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Databricks } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: secretScopes = [], isPending: isSecretScopesPending } = + useDatabricksConnectionListSecretScopes(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + <> + { + setValue("destinationConfig.scope", ""); + }} + /> + ( + +
+ Don't see the secret scope you're looking for?{" "} + +
+ + } + > + scope.name === value) ?? null} + onChange={(option) => + onChange((option as SingleValue)?.name ?? null) + } + options={secretScopes} + placeholder="Select a secret scope..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.name} + /> +
+ )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 19c3d7f7c..fa1aaa84c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -4,6 +4,10 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields"; +import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields"; +import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields"; +import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; +import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; @@ -15,10 +19,18 @@ export const SecretSyncDestinationFields = () => { switch (destination) { case SecretSync.AWSParameterStore: return ; + case SecretSync.AWSSecretsManager: + return ; case SecretSync.GitHub: return ; case SecretSync.GCPSecretManager: return ; + case SecretSync.AzureKeyVault: + return ; + case SecretSync.AzureAppConfiguration: + return ; + case SecretSync.Databricks: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/AwsRegionSelect.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/AwsRegionSelect.tsx new file mode 100644 index 000000000..4125393f5 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/AwsRegionSelect.tsx @@ -0,0 +1,41 @@ +import { components, OptionProps, SingleValue } from "react-select"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Badge, FilterableSelect } from "@app/components/v2"; +import { AWS_REGIONS } from "@app/helpers/appConnections"; + +const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => { + return ( + +
+

{children}

+ + {props.data.slug} + + {isSelected && ( + + )} +
+
+ ); +}; + +type Props = { + value: string; + onChange: (value: string | undefined) => void; +}; + +export const AwsRegionSelect = ({ value, onChange }: Props) => { + return ( + region.slug === value)} + onChange={(option) => onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)} + options={AWS_REGIONS} + placeholder="Select region..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.slug} + components={{ Option }} + /> + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/index.ts b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/index.ts new file mode 100644 index 000000000..5ebd689a0 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/shared/index.ts @@ -0,0 +1 @@ +export * from "./AwsRegionSelect"; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx new file mode 100644 index 000000000..d59fe9500 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields.tsx @@ -0,0 +1,39 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { Badge } from "@app/components/v2"; +import { AWS_REGIONS } from "@app/helpers/appConnections"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +export const AwsSecretsManagerSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AWSSecretsManager } + >(); + + const [region, mappingBehavior, secretName] = watch([ + "destinationConfig.region", + "destinationConfig.mappingBehavior", + "destinationConfig.secretName" + ]); + + const awsRegion = AWS_REGIONS.find((r) => r.slug === region); + + return ( + <> + + {awsRegion?.name} + + {awsRegion?.slug}{" "} + + + + {mappingBehavior} + + {mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && ( + {secretName} + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureAppConfigurationSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureAppConfigurationSyncReviewFields.tsx new file mode 100644 index 000000000..e318397ec --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureAppConfigurationSyncReviewFields.tsx @@ -0,0 +1,20 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const AzureAppConfigurationSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration } + >(); + const vaultBaseUrl = watch("destinationConfig.configurationUrl"); + const label = watch("destinationConfig.label"); + + return ( + <> + {vaultBaseUrl} + {label} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureKeyVaultSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureKeyVaultSyncReviewFields.tsx new file mode 100644 index 000000000..94a0b985e --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureKeyVaultSyncReviewFields.tsx @@ -0,0 +1,12 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const AzureKeyVaultSyncReviewFields = () => { + const { watch } = useFormContext(); + const vaultBaseUrl = watch("destinationConfig.vaultBaseUrl"); + + return {vaultBaseUrl}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields.tsx new file mode 100644 index 000000000..76de5ca54 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields.tsx @@ -0,0 +1,12 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const DatabricksSyncReviewFields = () => { + const { watch } = useFormContext(); + const scope = watch("destinationConfig.scope"); + + return {scope}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 6a0ddc280..61408cf93 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -3,11 +3,15 @@ import { useFormContext } from "react-hook-form"; import { SecretSyncLabel } from "@app/components/secret-syncs"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields"; +import { DatabricksSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields"; import { Badge } from "@app/components/v2"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields"; +import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; +import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; @@ -36,12 +40,24 @@ export const SecretSyncReviewFields = () => { case SecretSync.AWSParameterStore: DestinationFieldsComponent = ; break; + case SecretSync.AWSSecretsManager: + DestinationFieldsComponent = ; + break; case SecretSync.GitHub: DestinationFieldsComponent = ; break; case SecretSync.GCPSecretManager: DestinationFieldsComponent = ; break; + case SecretSync.AzureKeyVault: + DestinationFieldsComponent = ; + break; + case SecretSync.AzureAppConfiguration: + DestinationFieldsComponent = ; + break; + case SecretSync.Databricks: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema.ts new file mode 100644 index 000000000..b1f43e737 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +export const AwsSecretsManagerSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.AWSSecretsManager), + destinationConfig: z + .discriminatedUnion("mappingBehavior", [ + z.object({ + mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne) + }), + z.object({ + mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne), + secretName: z + .string() + .regex( + /^[a-zA-Z0-9/_+=.@-]+$/, + "Secret name must contain only alphanumeric characters and the characters /_+=.@-" + ) + .min(1, "Secret name is required") + .max(256, "Secret name cannot exceed 256 characters") + }) + ]) + .and( + z.object({ + region: z.string().min(1, "Region required") + }) + ) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts new file mode 100644 index 000000000..d472ddb04 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const AzureAppConfigurationSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.AzureAppConfiguration), + destinationConfig: z.object({ + configurationUrl: z + .string() + .trim() + .min(1, { message: "Azure App Configuration URL is required" }) + .url() + .refine( + (val) => val.endsWith(".azconfig.io"), + "URL should have the following format: https://resource-name-here.azconfig.io" + ), + label: z.string().optional() + }) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts new file mode 100644 index 000000000..11fa6279b --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const AzureKeyVaultSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.AzureKeyVault), + destinationConfig: z.object({ + vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required") + }) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts new file mode 100644 index 000000000..0a8f5e723 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/databricks-sync-destination-schema.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const DatabricksSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.Databricks), + destinationConfig: z.object({ + scope: z.string().trim().min(1, "Databricks scope required") + }) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index c3d517f82..f7d11dba4 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -1,10 +1,14 @@ import { z } from "zod"; +import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema"; +import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema"; import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; import { slugSchema } from "@app/lib/schemas"; import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema"; +import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema"; +import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; const BaseSecretSyncSchema = z.object({ @@ -32,8 +36,12 @@ const BaseSecretSyncSchema = z.object({ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncDestinationSchema, + AwsSecretsManagerSyncDestinationSchema, GitHubSyncDestinationSchema, - GcpSyncDestinationSchema + GcpSyncDestinationSchema, + AzureKeyVaultSyncDestinationSchema, + AzureAppConfigurationSyncDestinationSchema, + DatabricksSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema); diff --git a/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx b/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx index d7b78c67a..0afd1bb1f 100644 --- a/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx +++ b/frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx @@ -126,7 +126,7 @@ export type TBreadcrumbFormat = }; const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => ( -
+
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => { diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index f07aa2f2e..337011b38 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -1,15 +1,17 @@ import { ReactNode } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; type Props = { title: ReactNode; description?: ReactNode; children?: ReactNode; + className?: string; }; -export const PageHeader = ({ title, description, children }: Props) => ( -
+export const PageHeader = ({ title, description, children, className }: Props) => ( +
-
+

{title}

{children}
diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 42823af4d..6a41e42b8 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -46,9 +46,9 @@ export const ROUTE_PATHS = Object.freeze({ "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId" ), AppConnections: { - GithubOauthCallbackPage: setRoute( - "/organization/app-connections/github/oauth/callback", - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback" + OauthCallbackPage: setRoute( + "/organization/app-connections/$appConnection/oauth/callback", + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" ) } }, diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 1349395b8..016a86264 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -4,10 +4,13 @@ import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AwsConnectionMethod, + AzureAppConfigurationConnectionMethod, + AzureKeyVaultConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, TAppConnection } from "@app/hooks/api/appConnections/types"; +import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection"; export const APP_CONNECTION_MAP: Record = { [AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" }, @@ -15,13 +18,21 @@ export const APP_CONNECTION_MAP: Record { switch (method) { case GitHubConnectionMethod.App: return { name: "GitHub App", icon: faGithub }; + case AzureKeyVaultConnectionMethod.OAuth: + case AzureAppConfigurationConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth: return { name: "OAuth", icon: faPassport }; case AwsConnectionMethod.AccessKey: @@ -30,6 +41,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Assume Role", icon: faUser }; case GcpConnectionMethod.ServiceAccountImpersonation: return { name: "Service Account Impersonation", icon: faUser }; + case DatabricksConnectionMethod.ServicePrincipal: + return { name: "Service Principal", icon: faUser }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 905bae9da..b119983a0 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -6,15 +6,29 @@ import { } from "@app/hooks/api/secretSyncs"; export const SECRET_SYNC_MAP: Record = { - [SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" }, + [SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" }, + [SecretSync.AWSSecretsManager]: { name: "AWS Secrets Manager", image: "Amazon Web Services.png" }, [SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" }, - [SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" } + [SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" }, + [SecretSync.AzureKeyVault]: { name: "Azure Key Vault", image: "Microsoft Azure.png" }, + [SecretSync.AzureAppConfiguration]: { + name: "Azure App Configuration", + image: "Microsoft Azure.png" + }, + [SecretSync.Databricks]: { + name: "Databricks", + image: "Databricks.png" + } }; export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, + [SecretSync.AWSSecretsManager]: AppConnection.AWS, [SecretSync.GitHub]: AppConnection.GitHub, - [SecretSync.GCPSecretManager]: AppConnection.GCP + [SecretSync.GCPSecretManager]: AppConnection.GCP, + [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, + [SecretSync.Databricks]: AppConnection.Databricks }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/databricks/index.ts b/frontend/src/hooks/api/appConnections/databricks/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/databricks/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/databricks/queries.tsx b/frontend/src/hooks/api/appConnections/databricks/queries.tsx new file mode 100644 index 000000000..14f84cf9c --- /dev/null +++ b/frontend/src/hooks/api/appConnections/databricks/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections"; + +import { TDatabricksConnectionListSecretScopesResponse, TDatabricksSecretScope } from "./types"; + +const databricksConnectionKeys = { + all: [...appConnectionKeys.all, "databricks"] as const, + listSecretScopes: (connectionId: string) => + [...databricksConnectionKeys.all, "workspace-scopes", connectionId] as const +}; + +export const useDatabricksConnectionListSecretScopes = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TDatabricksSecretScope[], + unknown, + TDatabricksSecretScope[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: databricksConnectionKeys.listSecretScopes(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/databricks/${connectionId}/secret-scopes` + ); + + return data.secretScopes; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/databricks/types.ts b/frontend/src/hooks/api/appConnections/databricks/types.ts new file mode 100644 index 000000000..1a2c0cae1 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/databricks/types.ts @@ -0,0 +1,7 @@ +export type TDatabricksSecretScope = { + name: string; +}; + +export type TDatabricksConnectionListSecretScopesResponse = { + secretScopes: TDatabricksSecretScope[]; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index ba29a3781..97fcf9be6 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -1,5 +1,8 @@ export enum AppConnection { AWS = "aws", GitHub = "github", - GCP = "gcp" + GCP = "gcp", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration", + Databricks = "databricks" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 91fc25cc2..e42593c94 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -20,10 +20,33 @@ export type TGcpConnectionOption = TAppConnectionOptionBase & { app: AppConnection.GCP; }; -export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption; +export type TAzureKeyVaultConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.AzureKeyVault; + oauthClientId?: string; +}; + +export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.AzureKeyVault; + oauthClientId?: string; +}; + +export type TDatabricksConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Databricks; +}; + +export type TAppConnectionOption = + | TAwsConnectionOption + | TGitHubConnectionOption + | TGcpConnectionOption + | TAzureAppConfigurationConnectionOption + | TAzureKeyVaultConnectionOption + | TDatabricksConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; [AppConnection.GitHub]: TGitHubConnectionOption; [AppConnection.GCP]: TGcpConnectionOption; + [AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption; + [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption; + [AppConnection.Databricks]: TDatabricksConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/azure-app-configuration-connection.ts b/frontend/src/hooks/api/appConnections/types/azure-app-configuration-connection.ts new file mode 100644 index 000000000..b56003cad --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/azure-app-configuration-connection.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum AzureAppConfigurationConnectionMethod { + OAuth = "oauth" +} + +export type TAzureAppConfigurationConnection = TRootAppConnection & { + app: AppConnection.AzureAppConfiguration; +} & { + method: AzureAppConfigurationConnectionMethod.OAuth; + credentials: { + code: string; + tenantId?: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/azure-key-vault-connection.ts b/frontend/src/hooks/api/appConnections/types/azure-key-vault-connection.ts new file mode 100644 index 000000000..68888531a --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/azure-key-vault-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum AzureKeyVaultConnectionMethod { + OAuth = "oauth" +} + +export type TAzureKeyVaultConnection = TRootAppConnection & { app: AppConnection.AzureKeyVault } & { + method: AzureKeyVaultConnectionMethod.OAuth; + credentials: { + code: string; + tenantId?: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/databricks-connection.ts b/frontend/src/hooks/api/appConnections/types/databricks-connection.ts new file mode 100644 index 000000000..56774f9dd --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/databricks-connection.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "../enums"; +import { TRootAppConnection } from "./root-connection"; + +export enum DatabricksConnectionMethod { + ServicePrincipal = "service-principal" +} + +export type TDatabricksConnection = TRootAppConnection & { app: AppConnection.Databricks } & { + method: DatabricksConnectionMethod.ServicePrincipal; + credentials: { + workspaceUrl: string; + clientId: string; + clientSecret: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 283a6d1a0..a2a1b6792 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -1,17 +1,28 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-options"; import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection"; +import { TDatabricksConnection } from "@app/hooks/api/appConnections/types/databricks-connection"; import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection"; +import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection"; +import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; import { TGcpConnection } from "./gcp-connection"; export * from "./aws-connection"; +export * from "./azure-app-configuration-connection"; +export * from "./azure-key-vault-connection"; export * from "./gcp-connection"; export * from "./github-connection"; -export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection; +export type TAppConnection = + | TAwsConnection + | TGitHubConnection + | TGcpConnection + | TAzureKeyVaultConnection + | TAzureAppConfigurationConnection + | TDatabricksConnection; -export type TAvailableAppConnection = Pick; +export type TAvailableAppConnection = Pick; export type TListAppConnections = { appConnections: T[] }; export type TGetAppConnection = { appConnection: T }; @@ -40,4 +51,7 @@ export type TAppConnectionMap = { [AppConnection.AWS]: TAwsConnection; [AppConnection.GitHub]: TGitHubConnection; [AppConnection.GCP]: TGcpConnection; + [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection; + [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; + [AppConnection.Databricks]: TDatabricksConnection; }; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index db6bcb63d..a0624bc9b 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -4,6 +4,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.GET_SECRETS]: "List secrets", [EventType.GET_SECRET]: "Read secret", [EventType.DELETE_SECRETS]: "Delete secrets", + [EventType.CREATE_SECRETS]: "Create secrets", + [EventType.UPDATE_SECRETS]: "Update secrets", [EventType.CREATE_SECRET]: "Create secret", [EventType.UPDATE_SECRET]: "Update secret", [EventType.DELETE_SECRET]: "Delete secret", @@ -90,6 +92,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.UPDATE_CMEK]: "Update KMS key", [EventType.DELETE_CMEK]: "Delete KMS key", [EventType.GET_CMEKS]: "List KMS keys", + [EventType.GET_CMEK]: "Get KMS key", [EventType.CMEK_ENCRYPT]: "Encrypt with KMS key", [EventType.CMEK_DECRYPT]: "Decrypt with KMS key", [EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 185719f96..7f8b86549 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -20,6 +20,8 @@ export enum UserAgentType { export enum EventType { GET_SECRETS = "get-secrets", DELETE_SECRETS = "delete-secrets", + CREATE_SECRETS = "create-secrets", + UPDATE_SECRETS = "update-secrets", GET_SECRET = "get-secret", CREATE_SECRET = "create-secret", UPDATE_SECRET = "update-secret", @@ -105,6 +107,7 @@ export enum EventType { UPDATE_CMEK = "update-cmek", DELETE_CMEK = "delete-cmek", GET_CMEKS = "get-cmeks", + GET_CMEK = "get-cmek", CMEK_ENCRYPT = "cmek-encrypt", CMEK_DECRYPT = "cmek-decrypt", UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping", diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 11f902388..c942c388d 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -1,7 +1,11 @@ export enum SecretSync { AWSParameterStore = "aws-parameter-store", + AWSSecretsManager = "aws-secrets-manager", GitHub = "github", - GCPSecretManager = "gcp-secret-manager" + GCPSecretManager = "gcp-secret-manager", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration", + Databricks = "databricks" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/mutations.tsx b/frontend/src/hooks/api/secretSyncs/mutations.tsx index 1efab6688..bde4ce026 100644 --- a/frontend/src/hooks/api/secretSyncs/mutations.tsx +++ b/frontend/src/hooks/api/secretSyncs/mutations.tsx @@ -23,7 +23,8 @@ export const useCreateSecretSync = () => { return data.secretSync; }, - onSuccess: () => queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() }) + onSuccess: (_, { projectId }) => + queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) }) }); }; @@ -38,8 +39,8 @@ export const useUpdateSecretSync = () => { return data.secretSync; }, - onSuccess: (_, { syncId, destination }) => { - queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() }); + onSuccess: (_, { syncId, destination, projectId }) => { + queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) }); queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) }); } }); @@ -55,8 +56,8 @@ export const useDeleteSecretSync = () => { return data; }, - onSuccess: (_, { syncId, destination }) => { - queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() }); + onSuccess: (_, { syncId, destination, projectId }) => { + queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) }); queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) }); } }); @@ -72,8 +73,8 @@ export const useTriggerSecretSyncSyncSecrets = () => { return data; }, - onSuccess: (_, { syncId, destination }) => { - queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() }); + onSuccess: (_, { syncId, destination, projectId }) => { + queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) }); queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) }); } }); @@ -93,8 +94,8 @@ export const useTriggerSecretSyncImportSecrets = () => { return data; }, - onSuccess: (_, { syncId, destination }) => { - queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() }); + onSuccess: (_, { syncId, destination, projectId }) => { + queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) }); queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) }); } }); @@ -110,8 +111,8 @@ export const useTriggerSecretSyncRemoveSecrets = () => { return data; }, - onSuccess: (_, { syncId, destination }) => { - queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() }); + onSuccess: (_, { syncId, destination, projectId }) => { + queryClient.invalidateQueries({ queryKey: secretSyncKeys.list(projectId) }); queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) }); } }); diff --git a/frontend/src/hooks/api/secretSyncs/queries.tsx b/frontend/src/hooks/api/secretSyncs/queries.tsx index dbf444e20..bb0787a7b 100644 --- a/frontend/src/hooks/api/secretSyncs/queries.tsx +++ b/frontend/src/hooks/api/secretSyncs/queries.tsx @@ -12,7 +12,7 @@ import { export const secretSyncKeys = { all: ["secret-sync"] as const, options: () => [...secretSyncKeys.all, "options"] as const, - list: () => [...secretSyncKeys.all, "list"] as const, + list: (projectId: string) => [...secretSyncKeys.all, "list", projectId] as const, byId: (destination: SecretSync, syncId: string) => [...secretSyncKeys.all, destination, "by-id", syncId] as const }; @@ -46,6 +46,14 @@ export const useSecretSyncOption = (destination: SecretSync) => { return { syncOption, isPending }; }; +export const fetchSecretSyncsByProjectId = async (projectId: string) => { + const { data } = await apiRequest.get("/api/v1/secret-syncs", { + params: { projectId } + }); + + return data.secretSyncs; +}; + export const useListSecretSyncs = ( projectId: string, options?: Omit< @@ -54,14 +62,8 @@ export const useListSecretSyncs = ( > ) => { return useQuery({ - queryKey: secretSyncKeys.list(), - queryFn: async () => { - const { data } = await apiRequest.get("/api/v1/secret-syncs", { - params: { projectId } - }); - - return data.secretSyncs; - }, + queryKey: secretSyncKeys.list(projectId), + queryFn: () => fetchSecretSyncsByProjectId(projectId), ...options }); }; diff --git a/frontend/src/hooks/api/secretSyncs/types/aws-secrets-manager-sync.ts b/frontend/src/hooks/api/secretSyncs/types/aws-secrets-manager-sync.ts new file mode 100644 index 000000000..1bcf7c7df --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/aws-secrets-manager-sync.ts @@ -0,0 +1,26 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TAwsSecretsManagerSync = TRootSecretSync & { + destination: SecretSync.AWSSecretsManager; + destinationConfig: + | { + mappingBehavior: AwsSecretsManagerSyncMappingBehavior.OneToOne; + region: string; + } + | { + region: string; + mappingBehavior: AwsSecretsManagerSyncMappingBehavior.ManyToOne; + secretName: string; + }; + connection: { + app: AppConnection.AWS; + name: string; + id: string; + }; +}; +export enum AwsSecretsManagerSyncMappingBehavior { + OneToOne = "one-to-one", + ManyToOne = "many-to-one" +} diff --git a/frontend/src/hooks/api/secretSyncs/types/azure-app-configuration-sync.ts b/frontend/src/hooks/api/secretSyncs/types/azure-app-configuration-sync.ts new file mode 100644 index 000000000..535d2cb7c --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/azure-app-configuration-sync.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TAzureAppConfigurationSync = TRootSecretSync & { + destination: SecretSync.AzureAppConfiguration; + destinationConfig: { + configurationUrl: string; + label?: string; + }; + connection: { + app: AppConnection.AzureAppConfiguration; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/azure-key-vault-sync.ts b/frontend/src/hooks/api/secretSyncs/types/azure-key-vault-sync.ts new file mode 100644 index 000000000..7cdf57cb3 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/azure-key-vault-sync.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TAzureKeyVaultSync = TRootSecretSync & { + destination: SecretSync.AzureKeyVault; + destinationConfig: { + vaultBaseUrl: string; + }; + connection: { + app: AppConnection.AzureKeyVault; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/databricks-sync.ts b/frontend/src/hooks/api/secretSyncs/types/databricks-sync.ts new file mode 100644 index 000000000..982bcc059 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/databricks-sync.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TDatabricksSync = TRootSecretSync & { + destination: SecretSync.Databricks; + destinationConfig: { + scope: string; + }; + connection: { + app: AppConnection.Databricks; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 88c2a35da..e797c6458 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -1,8 +1,12 @@ import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs"; import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync"; +import { TDatabricksSync } from "@app/hooks/api/secretSyncs/types/databricks-sync"; import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync"; import { DiscriminativePick } from "@app/types"; +import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; +import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync"; +import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TGcpSync } from "./gcp-sync"; export type TSecretSyncOption = { @@ -11,7 +15,14 @@ export type TSecretSyncOption = { canImportSecrets: boolean; }; -export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync; +export type TSecretSync = + | TAwsParameterStoreSync + | TAwsSecretsManagerSync + | TGitHubSync + | TGcpSync + | TAzureKeyVaultSync + | TAzureAppConfigurationSync + | TDatabricksSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; @@ -34,26 +45,31 @@ export type TUpdateSecretSyncDTO = Partial< > & { destination: SecretSync; syncId: string; + projectId: string; }; export type TDeleteSecretSyncDTO = { destination: SecretSync; syncId: string; removeSecrets: boolean; + projectId: string; }; export type TTriggerSecretSyncSyncSecretsDTO = { destination: SecretSync; syncId: string; + projectId: string; }; export type TTriggerSecretSyncImportSecretsDTO = { destination: SecretSync; syncId: string; importBehavior: SecretSyncImportBehavior; + projectId: string; }; export type TTriggerSecretSyncRemoveSecretsDTO = { destination: SecretSync; syncId: string; + projectId: string; }; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 7ceeec450..60867fcf4 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -206,7 +206,7 @@ export const useGetWorkspaceAuthorizations = ( select }); -const fetchWorkspaceIntegrations = async (workspaceId: string) => { +export const fetchWorkspaceIntegrations = async (workspaceId: string) => { const { data } = await apiRequest.get<{ integrations: TIntegration[] }>( `/api/v1/workspace/${workspaceId}/integrations` ); diff --git a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx index bb69e6a62..589e82895 100644 --- a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx +++ b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx @@ -94,6 +94,13 @@ export const OrganizationLayout = () => { )} + + {({ isActive }) => ( + + App Connections + + )} + {({ isActive }) => ( diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 8a11b6ca0..273c4f15b 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -10,6 +10,7 @@ import { faInfo, faInfoCircle, faMoneyBill, + faPlug, faSignOut, faUser, faUsers @@ -100,6 +101,7 @@ export const MinimizedOrgSidebar = () => { const isMoreSelected = ( [ linkOptions({ to: "/organization/access-management" }).to, + linkOptions({ to: "/organization/app-connections" }).to, linkOptions({ to: "/organization/settings" }).to, linkOptions({ to: "/organization/audit-logs" }).to ] as string[] @@ -311,36 +313,43 @@ export const MinimizedOrgSidebar = () => {
- More + Org Controls
Organization Options - }> + }> Access Control + + }> + App Connections + + {(window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("https://eu.infisical.com") || window.location.origin.includes("https://gamma.infisical.com")) && ( - }> + } + > Usage & Billing )} - }> + }> Audit Logs - }> + }> Organization Settings diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx index 23e5b98a3..532d5afc6 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx @@ -284,7 +284,8 @@ export const CmekTable = () => { { navigator.clipboard.writeText(id); setCopyCipherText("Copied"); @@ -299,112 +300,116 @@ export const CmekTable = () => { {label} {version} - - - - - - - - - -
- handlePopUpOpen("encryptData", cmek)} - icon={} - iconPos="left" - isDisabled={cannotEncryptData || isDisabled} - > - Encrypt Data - -
-
- -
- handlePopUpOpen("decryptData", cmek)} - icon={} - iconPos="left" - isDisabled={cannotDecryptData || isDisabled} - > - Decrypt Data - -
-
- -
- handlePopUpOpen("upsertKey", cmek)} - icon={} - iconPos="left" - isDisabled={cannotEditKey} - > - Edit Key - -
-
- -
- handleDisableCmek(cmek)} - icon={ - - } - iconPos="left" - isDisabled={cannotEditKey} - > - {isDisabled ? "Enable" : "Disable"} Key - -
-
- -
- handlePopUpOpen("deleteKey", cmek)} - icon={} - iconPos="left" - isDisabled={cannotDeleteKey} - > - Delete Key - -
-
-
-
+ +
+ + + + + + + + +
+ handlePopUpOpen("encryptData", cmek)} + icon={} + iconPos="left" + isDisabled={cannotEncryptData || isDisabled} + > + Encrypt Data + +
+
+ +
+ handlePopUpOpen("decryptData", cmek)} + icon={} + iconPos="left" + isDisabled={cannotDecryptData || isDisabled} + > + Decrypt Data + +
+
+ +
+ handlePopUpOpen("upsertKey", cmek)} + icon={} + iconPos="left" + isDisabled={cannotEditKey} + > + Edit Key + +
+
+ +
+ handleDisableCmek(cmek)} + icon={ + + } + iconPos="left" + isDisabled={cannotEditKey} + > + {isDisabled ? "Enable" : "Disable"} Key + +
+
+ +
+ handlePopUpOpen("deleteKey", cmek)} + icon={} + iconPos="left" + isDisabled={cannotDeleteKey} + > + Delete Key + +
+
+
+
+
); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx new file mode 100644 index 000000000..ff3f31c9e --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/AppConnectionsPage.tsx @@ -0,0 +1,90 @@ +import { Helmet } from "react-helmet"; +import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, PageHeader } from "@app/components/v2"; +import { + OrgPermissionAppConnectionActions, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; +import { withPermission } from "@app/hoc"; +import { usePopUp } from "@app/hooks"; +import { + AddAppConnectionModal, + AppConnectionsTable +} from "@app/pages/organization/AppConnections/AppConnectionsPage/components"; + +export const AppConnectionsPage = withPermission( + () => { + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const); + + return ( +
+ + Infisical | App Connections + + + +
+
+ + App Connections + +
+ + Docs + +
+
+ + {(isAllowed) => ( + + )} + +
+ } + description="Create and configure connections with third-party apps for re-use across Infisical projects" + /> +
+ + handlePopUpToggle("addConnection", isOpen)} + /> +
+
+
+
+ ); + }, + { + action: OrgPermissionAppConnectionActions.Read, + subject: OrgPermissionSubjects.AppConnections + } +); diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AddAppConnectionModal.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AddAppConnectionModal.tsx rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AddAppConnectionModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx similarity index 81% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 99a18a6fc..c84b20885 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -10,6 +10,9 @@ import { DiscriminativePick } from "@app/types"; import { AppConnectionHeader } from "../AppConnectionHeader"; import { AwsConnectionForm } from "./AwsConnectionForm"; +import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm"; +import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; +import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; @@ -53,6 +56,12 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.GCP: return ; + case AppConnection.AzureKeyVault: + return ; + case AppConnection.AzureAppConfiguration: + return ; + case AppConnection.Databricks: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -92,6 +101,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.GCP: return ; + case AppConnection.AzureKeyVault: + return ; + case AppConnection.AzureAppConfiguration: + return ; + case AppConnection.Databricks: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AwsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AwsConnectionForm.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AwsConnectionForm.tsx rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AwsConnectionForm.tsx diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx new file mode 100644 index 000000000..75449fed3 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx @@ -0,0 +1,178 @@ +import crypto from "crypto"; + +import { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { isInfisicalCloud } from "@app/helpers/platform"; +import { + AzureAppConfigurationConnectionMethod, + TAzureAppConfigurationConnection, + useGetAppConnectionOption +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TAzureAppConfigurationConnection; +}; + +const formSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.AzureAppConfiguration), + method: z.nativeEnum(AzureAppConfigurationConnectionMethod), + tenantId: z.string().trim().optional() +}); + +type FormData = z.infer; + +export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) => { + const isUpdate = Boolean(appConnection); + const [isRedirecting, setIsRedirecting] = useState(false); + + const { + option: { oauthClientId }, + isLoading + } = useGetAppConnectionOption(AppConnection.AzureAppConfiguration); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection + ? { + ...appConnection, + tenantId: appConnection.credentials.tenantId + } + : { + app: AppConnection.AzureAppConfiguration, + method: AzureAppConfigurationConnectionMethod.OAuth + } + }); + + const { + handleSubmit, + control, + watch, + formState: { isSubmitting, isDirty } + } = form; + + const selectedMethod = watch("method"); + + const onSubmit = (formData: FormData) => { + setIsRedirecting(true); + const state = crypto.randomBytes(16).toString("hex"); + localStorage.setItem("latestCSRFToken", state); + localStorage.setItem( + "azureAppConfigurationConnectionFormData", + JSON.stringify({ ...formData, connectionId: appConnection?.id }) + ); + + switch (formData.method) { + case AzureAppConfigurationConnectionMethod.OAuth: + window.location.assign( + `https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-app-configuration` + ); + break; + default: + throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`); + } + }; + + let isMissingConfig: boolean; + + switch (selectedMethod) { + case AzureAppConfigurationConnectionMethod.OAuth: + isMissingConfig = !oauthClientId; + break; + default: + throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`); + } + + const methodDetails = getAppConnectionMethodDetails(selectedMethod); + + return ( + +
+ {!isUpdate && } + + ( + + + + )} + /> + + ( + + + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx new file mode 100644 index 000000000..58f9739ec --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx @@ -0,0 +1,178 @@ +import crypto from "crypto"; + +import { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { isInfisicalCloud } from "@app/helpers/platform"; +import { useGetAppConnectionOption } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + AzureKeyVaultConnectionMethod, + TAzureKeyVaultConnection +} from "@app/hooks/api/appConnections/types/azure-key-vault-connection"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TAzureKeyVaultConnection; +}; + +const formSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.AzureKeyVault), + method: z.nativeEnum(AzureKeyVaultConnectionMethod), + tenantId: z.string().trim().optional() +}); + +type FormData = z.infer; + +export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => { + const isUpdate = Boolean(appConnection); + const [isRedirecting, setIsRedirecting] = useState(false); + + const { + option: { oauthClientId }, + isLoading + } = useGetAppConnectionOption(AppConnection.AzureKeyVault); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection + ? { + ...appConnection, + tenantId: appConnection.credentials.tenantId + } + : { + app: AppConnection.AzureKeyVault, + method: AzureKeyVaultConnectionMethod.OAuth + } + }); + + const { + handleSubmit, + control, + watch, + formState: { isSubmitting, isDirty } + } = form; + + const selectedMethod = watch("method"); + + const onSubmit = (formData: FormData) => { + setIsRedirecting(true); + const state = crypto.randomBytes(16).toString("hex"); + localStorage.setItem("latestCSRFToken", state); + localStorage.setItem( + "azureKeyVaultConnectionFormData", + JSON.stringify({ ...formData, connectionId: appConnection?.id }) + ); + + switch (formData.method) { + case AzureKeyVaultConnectionMethod.OAuth: + window.location.assign( + `https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault` + ); + break; + default: + throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`); + } + }; + + let isMissingConfig: boolean; + + switch (selectedMethod) { + case AzureKeyVaultConnectionMethod.OAuth: + isMissingConfig = !oauthClientId; + break; + default: + throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`); + } + + const methodDetails = getAppConnectionMethodDetails(selectedMethod); + + return ( + +
+ {!isUpdate && } + + ( + + + + )} + /> + + ( + + + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DatabricksConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DatabricksConnectionForm.tsx new file mode 100644 index 000000000..997609d1b --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DatabricksConnectionForm.tsx @@ -0,0 +1,166 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + DatabricksConnectionMethod, + TDatabricksConnection +} from "@app/hooks/api/appConnections/types/databricks-connection"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TDatabricksConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Databricks) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(DatabricksConnectionMethod.ServicePrincipal), + credentials: z.object({ + workspaceUrl: z.string().url().trim().min(1, "Workspace URL required"), + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client secret required") + }) + }) +]); + +type FormData = z.infer; + +export const DatabricksConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Databricks, + method: DatabricksConnectionMethod.ServicePrincipal + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GcpConnectionForm.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GcpConnectionForm.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GenericAppConnectionFields.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GenericAppConnectionFields.tsx rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GitHubConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GitHubConnectionForm.tsx rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/index.ts b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/index.ts similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/index.ts rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/index.ts diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionHeader.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionHeader.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionHeader.tsx rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionHeader.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionList.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx similarity index 96% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionList.tsx rename to frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx index 285144875..e6abba757 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionList.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx @@ -11,9 +11,9 @@ type Props = { }; export const AppConnectionsSelect = ({ onSelect }: Props) => { - const { isLoading, data: appConnectionOptions } = useAppConnectionOptions(); + const { isPending, data: appConnectionOptions } = useAppConnectionOptions(); - if (isLoading) { + if (isPending) { return (
@@ -23,7 +23,7 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => { } return ( -
+
{appConnectionOptions?.map((option) => ( - )} - -
- - handlePopUpToggle("addConnection", isOpen)} - /> -
-
- ); - }, - { - action: OrgPermissionAppConnectionActions.Read, - subject: OrgPermissionSubjects.AppConnections - } -); diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/index.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/index.tsx deleted file mode 100644 index 79649edaf..000000000 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { AppConnectionsTab } from "./AppConnectionsTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index 303d959de..31f8efb0c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -4,7 +4,6 @@ import { useSearch } from "@tanstack/react-router"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { AppConnectionsTab } from "../AppConnectionsTab"; import { AuditLogStreamsTab } from "../AuditLogStreamTab"; import { ImportTab } from "../ImportTab"; import { KmipTab } from "../KmipTab/OrgKmipTab"; @@ -27,7 +26,6 @@ export const OrgTabGroup = () => { key: "workflow-integrations", component: OrgWorkflowIntegrationTab }, - { name: "App Connections", key: "app-connections", component: AppConnectionsTab }, { name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab }, { name: "Import", key: "tab-import", component: ImportTab }, { name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }, diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx index 492df2aaa..9afedcbc5 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx @@ -1,13 +1,19 @@ import { useMemo } from "react"; -import { Controller, useForm } from "react-hook-form"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Link } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2"; -import { useOrganization, useWorkspace } from "@app/context"; +import { CreatableSelect } from "@app/components/v2/CreatableSelect"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission, + useWorkspace +} from "@app/context"; import { useAddUsersToOrg, useGetOrgUsers, @@ -19,7 +25,15 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; const addMemberFormSchema = z.object({ - orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1), + orgMemberships: z + .array( + z.object({ + label: z.string().trim(), + value: z.string().trim(), + isNewInvitee: z.boolean().optional() + }) + ) + .min(1), projectRoleSlugs: z.array(z.object({ slug: z.string().trim(), name: z.string().trim() })).min(1) }); @@ -34,6 +48,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { const { t } = useTranslation(); const { currentOrg } = useOrganization(); const { currentWorkspace } = useWorkspace(); + const { permission } = useOrgPermission(); const orgId = currentOrg?.id || ""; const workspaceId = currentWorkspace?.id || ""; @@ -60,7 +75,12 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { if (!currentWorkspace) return; if (!currentOrg?.id) return; - const selectedMembers = orgMemberships.map((orgMembership) => + const existingMembers = orgMemberships.filter((membership) => !membership.isNewInvitee); + const newInvitees = orgMemberships + .filter((membership) => membership.isNewInvitee) + .map((membership) => membership.value); + + const selectedMembers = existingMembers.map((orgMembership) => orgUsers?.find((orgUser) => orgUser.id === orgMembership.value) ); @@ -76,11 +96,11 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { const inviteeEmails = selectedMembers .map((member) => member?.user.username as string) .filter(Boolean); - if (inviteeEmails.length) { + if (inviteeEmails.length || newInvitees.length) { await addMembersToProject({ - inviteeEmails, + inviteeEmails: [...inviteeEmails, ...newInvitees], organizationId: orgId, - organizationRoleSlug: ProjectMembershipRole.Member, // ? This doesn't apply in this case, because we know the users being added are already part of the organization + organizationRoleSlug: ProjectMembershipRole.Member, // only applies to new invites projects: [ { slug: currentWorkspace.slug, @@ -125,6 +145,13 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { const selectedOrgMemberships = watch("orgMemberships"); const selectedRoleSlugs = watch("projectRoleSlugs"); + const { append } = useFieldArray({ control, name: "orgMemberships" }); + + const canInviteNewMembers = permission.can( + OrgPermissionActions.Create, + OrgPermissionSubjects.Member + ); + return ( { title={t("section.members.add-dialog.add-member-to-project") as string} subTitle={t("section.members.add-dialog.user-will-email")} > - {filteredOrgUsers.length ? ( -
-
- ( - + +
+ ( + + {canInviteNewMembers ? ( + ( + <> +

+ {!filteredOrgUsers.length && ( +

All organization members are already assigned to this project.

+ )} +

+

+ Invite new users to your organization by typing out their email address. +

+ + )} + onCreateOption={(inputValue) => + append({ label: inputValue, value: inputValue, isNewInvitee: true }) + } + formatCreateLabel={(inputValue) => `Invite "${inputValue}"`} + isValidNewOption={(input) => + Boolean(input) && + z.string().email().safeParse(input).success && + !orgUsers + ?.flatMap(({ user }) => { + const emails: string[] = []; + + if (user.email) { + emails.push(user.email); + } + + if (user.username) { + emails.push(user.username); + } + + return emails; + }) + .includes(input) + } + className="w-full" + placeholder="Add one or more users..." + isMulti + name="members" + options={filteredOrgUsers} + value={field.value} + onChange={field.onChange} + /> + ) : ( { value={field.value} onChange={field.onChange} /> -
- )} - /> + )} + + )} + /> - ( - - option.slug} - getOptionLabel={(option) => option.name} - /> - - )} - /> -
-
- - -
- - ) : ( -
-
All the users in your organization are already invited.
- - - + ( + + option.slug} + getOptionLabel={(option) => option.name} + /> + + )} + />
- )} +
+ + +
+ ); diff --git a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx index ba6b8834e..6f13e5bd7 100644 --- a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx @@ -2,12 +2,15 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useParams } from "@tanstack/react-router"; +import { useNavigate, useParams } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; +import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button, + Checkbox, + DeleteActionModal, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -18,8 +21,10 @@ import { } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context"; +import { usePopUp, useToggle } from "@app/hooks"; import { useGetIntegration } from "@app/hooks/api"; -import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; +import { useDeleteIntegration, useSyncIntegration } from "@app/hooks/api/integrations/queries"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { IntegrationAuditLogsSection } from "./components/IntegrationAuditLogsSection"; import { IntegrationConnectionSection } from "./components/IntegrationConnectionSection"; @@ -38,9 +43,44 @@ export const IntegrationDetailsByIDPage = () => { refetchInterval: 4000 }); + const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false); + const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace.id; const { mutateAsync: syncIntegration } = useSyncIntegration(); + const { mutateAsync: deleteIntegration } = useDeleteIntegration(); + + const navigate = useNavigate(); + + const handleIntegrationDelete = async (shouldDeleteIntegrationSecrets: boolean) => { + try { + await deleteIntegration({ + id: integrationId, + workspaceId: currentWorkspace.id, + shouldDeleteIntegrationSecrets + }); + + createNotification({ + type: "success", + text: "Deleted integration" + }); + + await navigate({ + to: `/${ProjectType.SecretManager}/${projectId}/integrations` + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to delete integration" + }); + } + }; + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "deleteConfirmation", + "deleteSecretsConfirmation" + ] as const); return ( <> @@ -90,7 +130,10 @@ export const IntegrationDetailsByIDPage = () => { ? "hover:!bg-red-500 hover:!text-white" : "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={() => {}} + onClick={() => { + setShouldDeleteSecrets.off(); + handlePopUpOpen("deleteConfirmation", integration); + }} disabled={!isAllowed} >
@@ -120,6 +163,57 @@ export const IntegrationDetailsByIDPage = () => {
)}
+ + handlePopUpToggle("deleteConfirmation", isOpen)} + deleteKey={ + (integration?.integration === "azure-app-configuration" && + integration?.app?.split("//")[1]?.split(".")[0]) || + integration?.app || + integration?.owner || + integration?.path || + integration?.integration || + "" + } + onDeleteApproved={async () => { + if (shouldDeleteSecrets) { + handlePopUpOpen("deleteSecretsConfirmation"); + return; + } + + await handleIntegrationDelete(false); + + handlePopUpClose("deleteConfirmation"); + }} + > + {integration?.integration === "github" && ( +
+ setShouldDeleteSecrets.toggle()} + > + Delete previously synced secrets from the destination + +
+ )} +
+ handlePopUpToggle("deleteSecretsConfirmation", isOpen)} + deleteKey="confirm" + onDeleteApproved={async () => { + await handleIntegrationDelete(true); + handlePopUpClose("deleteSecretsConfirmation"); + handlePopUpClose("deleteConfirmation"); + }} + /> ); }; diff --git a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx index 94290372e..916812e66 100644 --- a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { IntegrationDetailsByIDPage } from "./IntegrationsDetailsByIDPage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx index 241e673ca..ac53cb4d9 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx @@ -8,6 +8,7 @@ import { ProjectPermissionCan } from "@app/components/permissions"; import { Badge, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types"; import { IntegrationsListPageTabs } from "@app/types/integrations"; import { @@ -29,7 +30,7 @@ export const IntegrationsListPage = () => { const updateSelectedTab = (tab: string) => { navigate({ to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, - search: (prev) => ({ ...prev, selectedTab: tab }), + search: (prev) => ({ ...prev, selectedTab: tab as IntegrationsListPageTabs }), params: { projectId: currentWorkspace.id } @@ -52,7 +53,7 @@ export const IntegrationsListPage = () => { />
- + Integrations Update

@@ -100,8 +101,7 @@ export const IntegrationsListPage = () => { @@ -110,7 +110,6 @@ export const IntegrationsListPage = () => { diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AwsSecretsManagerSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AwsSecretsManagerSyncDestinationCol.tsx new file mode 100644 index 000000000..9cfe9af2b --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AwsSecretsManagerSyncDestinationCol.tsx @@ -0,0 +1,43 @@ +import { + AwsSecretsManagerSyncMappingBehavior, + TAwsSecretsManagerSync +} from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell, SecretSyncTableCellProps } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TAwsSecretsManagerSync; +}; + +export const AwsSecretsManagerSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + const { destinationConfig } = secretSync; + + let additionalProps: Pick< + SecretSyncTableCellProps, + "additionalTooltipContent" | "infoBadge" | "secondaryClassName" + > = {}; + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne) { + additionalProps = { + infoBadge: "secondary", + additionalTooltipContent: ( +

+ Secret Name: +

{destinationConfig.secretName}

+
+ ) + }; + } + + return ( + + ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureAppConfigurationDestinationSyncCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureAppConfigurationDestinationSyncCol.tsx new file mode 100644 index 000000000..5a80a5b35 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureAppConfigurationDestinationSyncCol.tsx @@ -0,0 +1,14 @@ +import { TAzureAppConfigurationSync } from "@app/hooks/api/secretSyncs/types/azure-app-configuration-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TAzureAppConfigurationSync; +}; + +export const AzureAppConfigurationDestinationSyncCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureKeyVaultDestinationSyncCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureKeyVaultDestinationSyncCol.tsx new file mode 100644 index 000000000..149861d33 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureKeyVaultDestinationSyncCol.tsx @@ -0,0 +1,14 @@ +import { TAzureKeyVaultSync } from "@app/hooks/api/secretSyncs/types/azure-key-vault-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TAzureKeyVaultSync; +}; + +export const AzureKeyVaultDestinationSyncCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DatabricksSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DatabricksSyncDestinationCol.tsx new file mode 100644 index 000000000..91d20a71a --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DatabricksSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TDatabricksSync } from "@app/hooks/api/secretSyncs/types/databricks-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TDatabricksSync; +}; + +export const DatabricksSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 673e2aced..2c375a8b5 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -1,6 +1,10 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol"; +import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol"; +import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol"; +import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; +import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; @@ -12,10 +16,18 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { switch (secretSync.destination) { case SecretSync.AWSParameterStore: return ; + case SecretSync.AWSSecretsManager: + return ; case SecretSync.GitHub: return ; case SecretSync.GCPSecretManager: return ; + case SecretSync.AzureKeyVault: + return ; + case SecretSync.AzureAppConfiguration: + return ; + case SecretSync.Databricks: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncRow.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncRow.tsx index 3ef92d09a..02fc12027 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncRow.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncRow.tsx @@ -208,7 +208,9 @@ export const SecretSyncRow = ({
Failure Reason
-
{failureMessage}
+
+ {failureMessage} +
)}
diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncTableCell.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncTableCell.tsx index 6f42c1f0e..50d36d606 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncTableCell.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncTableCell.tsx @@ -30,7 +30,11 @@ export const SecretSyncTableCell = ({ content={ <>

{primaryText}

- {secondaryText &&

{secondaryText}

} + {secondaryText && ( +

+ {secondaryText} +

+ )} {additionalTooltipContent} } diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx index fefb1ad41..802e88bc1 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx @@ -227,7 +227,8 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => { await updateSync.mutateAsync({ syncId: secretSync.id, destination: secretSync.destination, - isAutoSyncEnabled + isAutoSyncEnabled, + projectId: secretSync.projectId }); createNotification({ @@ -248,7 +249,8 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => { try { await triggerSync.mutateAsync({ syncId: secretSync.id, - destination: secretSync.destination + destination: secretSync.destination, + projectId: secretSync.projectId }); createNotification({ diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 94147aa26..f1087c03c 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -17,6 +17,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.path; secondaryText = destinationConfig.region; break; + case SecretSync.AWSSecretsManager: + primaryText = destinationConfig.region; + secondaryText = destinationConfig.mappingBehavior; + break; case SecretSync.GitHub: switch (destinationConfig.scope) { case GitHubSyncScope.Organization: @@ -43,6 +47,18 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectId; secondaryText = "Global"; break; + case SecretSync.AzureKeyVault: + primaryText = destinationConfig.vaultBaseUrl; + break; + case SecretSync.AzureAppConfiguration: + primaryText = destinationConfig.configurationUrl; + if (destinationConfig.label) { + secondaryText = `Label - ${destinationConfig.label}`; + } + break; + case SecretSync.Databricks: + primaryText = destinationConfig.scope; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx index 5a176b2f9..7a5f70622 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx @@ -1,15 +1,21 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, redirect } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { workspaceKeys } from "@app/hooks/api"; +import { TIntegration } from "@app/hooks/api/integrations/types"; +import { + fetchSecretSyncsByProjectId, + secretSyncKeys, + TSecretSync +} from "@app/hooks/api/secretSyncs"; +import { fetchWorkspaceIntegrations } from "@app/hooks/api/workspace/queries"; import { IntegrationsListPageTabs } from "@app/types/integrations"; import { IntegrationsListPage } from "./IntegrationsListPage"; const IntegrationsListPageQuerySchema = z.object({ - selectedTab: z - .nativeEnum(IntegrationsListPageTabs) - .catch(IntegrationsListPageTabs.NativeIntegrations) + selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional() }); export const Route = createFileRoute( @@ -17,7 +23,70 @@ export const Route = createFileRoute( )({ component: IntegrationsListPage, validateSearch: zodValidator(IntegrationsListPageQuerySchema), - beforeLoad: ({ context }) => { + beforeLoad: async ({ context, search, params: { projectId } }) => { + if (!search.selectedTab) { + let secretSyncs: TSecretSync[]; + + try { + secretSyncs = await context.queryClient.ensureQueryData({ + queryKey: secretSyncKeys.list(projectId), + queryFn: () => fetchSecretSyncsByProjectId(projectId) + }); + } catch { + throw redirect({ + to: "/secret-manager/$projectId/integrations", + params: { + projectId + }, + search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations } + }); + } + + if (secretSyncs.length) { + throw redirect({ + to: "/secret-manager/$projectId/integrations", + params: { + projectId + }, + search: { selectedTab: IntegrationsListPageTabs.SecretSyncs } + }); + } + + let integrations: TIntegration[]; + try { + integrations = await context.queryClient.ensureQueryData({ + queryKey: workspaceKeys.getWorkspaceIntegrations(projectId), + queryFn: () => fetchWorkspaceIntegrations(projectId) + }); + } catch { + throw redirect({ + to: "/secret-manager/$projectId/integrations", + params: { + projectId + }, + search: { selectedTab: IntegrationsListPageTabs.SecretSyncs } + }); + } + + if (integrations.length) { + throw redirect({ + to: "/secret-manager/$projectId/integrations", + params: { + projectId + }, + search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations } + }); + } + + throw redirect({ + to: "/secret-manager/$projectId/integrations", + params: { + projectId + }, + search: { selectedTab: IntegrationsListPageTabs.SecretSyncs } + }); + } + return { breadcrumbs: [ ...context.breadcrumbs, diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncActionTriggers.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncActionTriggers.tsx index bc4f6d2b8..bda6e55e3 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncActionTriggers.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncActionTriggers.tsx @@ -92,7 +92,8 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => { await updateSync.mutateAsync({ syncId: secretSync.id, destination: secretSync.destination, - isAutoSyncEnabled + isAutoSyncEnabled, + projectId: secretSync.projectId }); createNotification({ @@ -111,7 +112,8 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => { try { await triggerSyncSecrets.mutateAsync({ syncId: secretSync.id, - destination: secretSync.destination + destination: secretSync.destination, + projectId: secretSync.projectId }); createNotification({ diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection.tsx new file mode 100644 index 000000000..5b73a4492 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection.tsx @@ -0,0 +1,34 @@ +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { Badge } from "@app/components/v2"; +import { AWS_REGIONS } from "@app/helpers/appConnections"; +import { + AwsSecretsManagerSyncMappingBehavior, + TAwsSecretsManagerSync +} from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; + +type Props = { + secretSync: TAwsSecretsManagerSync; +}; + +export const AwsSecretsManagerSyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + const awsRegion = AWS_REGIONS.find((r) => r.slug === destinationConfig.region); + + return ( + <> + + {awsRegion?.name} + + {awsRegion?.slug}{" "} + + + + {destinationConfig.mappingBehavior} + + {destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && ( + {destinationConfig.secretName} + )} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx new file mode 100644 index 000000000..7db01f7fe --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx @@ -0,0 +1,19 @@ +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TAzureAppConfigurationSync } from "@app/hooks/api/secretSyncs/types/azure-app-configuration-sync"; + +type Props = { + secretSync: TAzureAppConfigurationSync; +}; + +export const AzureAppConfigurationSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { configurationUrl, label } + } = secretSync; + + return ( + <> + {configurationUrl} + {label} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureKeyVaultSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureKeyVaultSyncDestinationSection.tsx new file mode 100644 index 000000000..4a30e6e08 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureKeyVaultSyncDestinationSection.tsx @@ -0,0 +1,14 @@ +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TAzureKeyVaultSync } from "@app/hooks/api/secretSyncs/types/azure-key-vault-sync"; + +type Props = { + secretSync: TAzureKeyVaultSync; +}; + +export const AzureKeyVaultSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { vaultBaseUrl } + } = secretSync; + + return {vaultBaseUrl}; +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DatabricksSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DatabricksSyncDestinationSection.tsx new file mode 100644 index 000000000..ecc844471 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DatabricksSyncDestinationSection.tsx @@ -0,0 +1,14 @@ +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TDatabricksSync } from "@app/hooks/api/secretSyncs/types/databricks-sync"; + +type Props = { + secretSync: TDatabricksSync; +}; + +export const DatabricksSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { scope } + } = secretSync; + + return {scope}; +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 4bfbd492a..b7d3964c7 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -10,8 +10,12 @@ import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissi import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection"; +import { AwsSecretsManagerSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection"; +import { DatabricksSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DatabricksSyncDestinationSection"; import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection"; +import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection"; +import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; type Props = { @@ -29,12 +33,26 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.AWSParameterStore: DestinationComponents = ; break; + case SecretSync.AWSSecretsManager: + DestinationComponents = ; + break; case SecretSync.GitHub: DestinationComponents = ; break; case SecretSync.GCPSecretManager: DestinationComponents = ; break; + case SecretSync.AzureKeyVault: + DestinationComponents = ; + break; + case SecretSync.AzureAppConfiguration: + DestinationComponents = ( + + ); + break; + case SecretSync.Databricks: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/route.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/route.tsx index 7fcade485..8e76138ea 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/route.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { SecretSyncDetailsByIDPage } from "./SecretSyncDetailsByIDPage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.SecretSyncs + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/route.tsx index c885d2e10..f066497ff 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AWSParameterStoreAuthorizeIntegrationPage } from "./AwsParameterStoreAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/AwsParamterStoreConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/AwsParamterStoreConfigurePage.tsx index 3bf787763..efb8573b3 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/AwsParamterStoreConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/AwsParamterStoreConfigurePage.tsx @@ -30,6 +30,7 @@ import { useWorkspace } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; enum TabSections { Connection = "connection", @@ -158,6 +159,9 @@ export const AWSParameterStoreConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/route.tsx index ce686c860..9195a08b8 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsParameterStoreConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AWSParameterStoreConfigurePage } from "./AwsParamterStoreConfigurePage"; const AwsParameterStoreConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerAuthorizePage/route.tsx index 67230282e..6f56da795 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AWSSecretManagerAuthorizePage } from "./AwsSecretManagerAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx index 2a5bdbf70..2297575d7 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx @@ -38,6 +38,7 @@ import { IntegrationMappingBehavior, IntegrationMetadataSyncMode } from "@app/hooks/api/integrations/types"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; enum TabSections { Connection = "connection", @@ -206,6 +207,9 @@ export const AwsSecretManagerConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/route.tsx index 7275bc2aa..97df1702c 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AwsSecretManagerConfigurePage } from "./AwsSecretManagerConfigurePage"; const AwsSecretManagerConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationConfigurePage/AzureAppConfigurationConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationConfigurePage/AzureAppConfigurationConfigurePage.tsx index 792ed9010..7485068ab 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationConfigurePage/AzureAppConfigurationConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationConfigurePage/AzureAppConfigurationConfigurePage.tsx @@ -26,6 +26,7 @@ import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; import { useGetWorkspaceById } from "@app/hooks/api/workspace"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const schema = z.object({ baseUrl: z @@ -133,6 +134,9 @@ export const AzureAppConfigurationConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationConfigurePage/route.tsx index 020941e0c..250160f4f 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AzureAppConfigurationConfigurePage } from "./AzureAppConfigurationConfigurePage"; const AzureAppConfigurationPageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/route.tsx index 5a94048d5..d8371fcf4 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AzureAppConfigurationOauthCallbackPage } from "./AzureAppConfigurationOauthCallbackPage"; export const AzureAppConfigurationOauthCallbackPageQueryParamsSchema = z.object({ @@ -22,7 +24,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/route.tsx index 4dca8b8c6..c9284f800 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AzureDevopsAuthorizePage } from "./AzureDevopsAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/AzureDevopsConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/AzureDevopsConfigurePage.tsx index 7935b615b..7615909ad 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/AzureDevopsConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/AzureDevopsConfigurePage.tsx @@ -21,6 +21,7 @@ import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { useGetWorkspaceById } from "@app/hooks/api/workspace"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const AzureDevopsConfigurePage = () => { const navigate = useNavigate(); @@ -82,6 +83,9 @@ export const AzureDevopsConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/route.tsx index 620e71660..766963366 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureDevopsConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AzureDevopsConfigurePage } from "./AzureDevopsConfigurePage"; const AzureDevopsConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultAuthorizePage/route.tsx index 6d14a4bb7..3975e7da1 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultAuthorizePage/route.tsx @@ -1,6 +1,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import z from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AzureKeyVaultAuthorizePage } from "./AzureKeyVaultAuthorizePage"; const PageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/AzureKeyVaultConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/AzureKeyVaultConfigurePage.tsx index aaf65289a..4dd3c4a11 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/AzureKeyVaultConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/AzureKeyVaultConfigurePage.tsx @@ -16,6 +16,7 @@ import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; import { useGetWorkspaceById } from "@app/hooks/api/workspace"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const initialSyncBehaviors = [ { @@ -90,6 +91,9 @@ export const AzureKeyVaultConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/route.tsx index 773fa5468..76a945ee3 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AzureKeyVaultConfigurePage } from "./AzureKeyVaultConfigurePage"; const AzureKeyVaultConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/route.tsx index 2e3e03455..c2239f48e 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { AzureKeyVaultOauthCallbackPage } from "./AzureKeyVaultOauthCallback"; export const AzureKeyVaultOauthCallbackQueryParamsSchema = z.object({ @@ -22,7 +24,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx index 5d4d9710b..abf8d9f02 100644 --- a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/BitbucketConfigurePage.tsx @@ -23,6 +23,7 @@ import { useGetIntegrationAuthBitBucketWorkspaces } from "@app/hooks/api"; import { useGetIntegrationAuthBitBucketEnvironments } from "@app/hooks/api/integrationAuth/queries"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; enum BitbucketScope { Repo = "repo", @@ -131,6 +132,9 @@ export const BitbucketConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/route.tsx index d10fbbc80..68b1acd63 100644 --- a/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/BitbucketConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { BitbucketConfigurePage } from "./BitbucketConfigurePage"; const BitbucketConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/BitbucketOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/BitbucketOauthCallbackPage/route.tsx index be898d684..d02697846 100644 --- a/frontend/src/pages/secret-manager/integrations/BitbucketOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/BitbucketOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { BitbucketOauthCallbackPage } from "./BitbucketOauthCallbackPage"; export const BitbucketOauthCallbackQueryParamsSchema = z.object({ @@ -22,7 +24,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/ChecklyAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/ChecklyAuthorizePage/route.tsx index cd38ff7ec..7b54ba00e 100644 --- a/frontend/src/pages/secret-manager/integrations/ChecklyAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/ChecklyAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { ChecklyAuthorizePage } from "./ChecklyAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/ChecklyConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/ChecklyConfigurePage.tsx index 99b182bfe..63169a3d1 100644 --- a/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/ChecklyConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/ChecklyConfigurePage.tsx @@ -27,6 +27,7 @@ import { useGetIntegrationAuthChecklyGroups } from "@app/hooks/api/integrationAuth"; import { useGetWorkspaceById } from "@app/hooks/api/workspace"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; enum TabSections { Connection = "connection", @@ -115,6 +116,9 @@ export const ChecklyConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/route.tsx index 832ecc202..652aae23a 100644 --- a/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/ChecklyConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { ChecklyConfigurePage } from "./ChecklyConfigurePage"; const ChecklyConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/CircleCIAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/CircleCIAuthorizePage/route.tsx index 0fb0e577b..b625dcce4 100644 --- a/frontend/src/pages/secret-manager/integrations/CircleCIAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/CircleCIAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { CircleCIAuthorizePage } from "./CircleCIAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx index e91a3ca89..9ba050808 100644 --- a/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/CircleCIConfigurePage.tsx @@ -21,6 +21,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { useWorkspace } from "@app/context"; import { useCreateIntegration, useGetIntegrationAuthCircleCIOrganizations } from "@app/hooks/api"; import { CircleCiScope } from "@app/hooks/api/integrationAuth/types"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const formSchema = z.discriminatedUnion("scope", [ z.object({ @@ -105,6 +106,9 @@ export const CircleCIConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/route.tsx index 7f3ed1d37..0c6bb61f0 100644 --- a/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/CircleCIConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { CircleCIConfigurePage } from "./CircleCIConfigurePage"; const CircleCIConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/Cloud66AuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/Cloud66AuthorizePage/route.tsx index b0dd2c467..5b6e06c55 100644 --- a/frontend/src/pages/secret-manager/integrations/Cloud66AuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/Cloud66AuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { Cloud66AuthorizePage } from "./Cloud66AuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/Cloud66ConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/Cloud66ConfigurePage.tsx index 73c410ce2..ace6e4f6c 100644 --- a/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/Cloud66ConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/Cloud66ConfigurePage.tsx @@ -17,6 +17,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const Cloud66ConfigurePage = () => { const navigate = useNavigate(); @@ -77,6 +78,9 @@ export const Cloud66ConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/route.tsx index 422d0ce5f..1c7fe58d2 100644 --- a/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/Cloud66ConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { Cloud66ConfigurePage } from "./Cloud66ConfigurePage"; const Cloud66ConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/CloudflarePagesAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/CloudflarePagesAuthorizePage/route.tsx index 4f216cccb..10a49dbf8 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflarePagesAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflarePagesAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { CloudflarePagesAuthorizePage } from "./CloudflarePagesAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx index 03be1dab6..1d9c188e6 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/CloudflarePagesConfigurePage.tsx @@ -20,6 +20,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const cloudflareEnvironments = [ { name: "Production", slug: "production" }, @@ -96,6 +97,9 @@ export const CloudflarePagesConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/route.tsx index 29310625e..30be97266 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflarePagesConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { CloudflarePagesConfigurePage } from "./CloudflarePagesConfigurePage"; const CloudflarePagesConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersAuthorizePage/route.tsx index 0558baed2..731053ce0 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { CloudflareWorkersAuthorizePage } from "./CloudflareWorkersAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx index 0f2149633..c650c47af 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/CloudflareWorkersConfigurePage.tsx @@ -12,6 +12,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const CloudflareWorkersConfigurePage = () => { const navigate = useNavigate(); @@ -69,6 +70,9 @@ export const CloudflareWorkersConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/route.tsx index 1687628e3..ffbb56a83 100644 --- a/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/CloudflareWorkersConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { CloudflareWorkersConfigurePage } from "./CloudflareWorkersConfigurePage"; const CloudflareWorkersConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/CodefreshAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/CodefreshAuthorizePage/route.tsx index 443fc2267..b414b9819 100644 --- a/frontend/src/pages/secret-manager/integrations/CodefreshAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/CodefreshAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { CodefreshAuthorizePage } from "./CodefreshAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/CodefreshConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/CodefreshConfigurePage.tsx index 1961236d9..23dbcabf1 100644 --- a/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/CodefreshConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/CodefreshConfigurePage.tsx @@ -17,6 +17,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const CodefreshConfigurePage = () => { const navigate = useNavigate(); @@ -73,6 +74,9 @@ export const CodefreshConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/route.tsx index 717462908..5ddde68e6 100644 --- a/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/CodefreshConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { CodefreshConfigurePage } from "./CodefreshConfigurePage"; const CodefreshConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/DatabricksAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/DatabricksAuthorizePage/route.tsx index a49d94583..4fdee2ba6 100644 --- a/frontend/src/pages/secret-manager/integrations/DatabricksAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/DatabricksAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { DatabricksAuthorizePage } from "./DatabricksAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx index 73453c80a..69538a318 100644 --- a/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/DatabricksConfigurePage.tsx @@ -26,6 +26,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const DatabricksConfigurePage = () => { const navigate = useNavigate(); @@ -89,6 +90,9 @@ export const DatabricksConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/route.tsx index 76bb65f1f..e7e8ae2db 100644 --- a/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/DatabricksConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { DatabricksConfigurePage } from "./DatabricksConfigurePage"; const DatabricksConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformAuthorizePage/route.tsx index 68277ce59..5b3a62175 100644 --- a/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { DigitalOceanAppPlatformAuthorizePage } from "./DigitalOceanAppPlatformAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/DigitalOceanAppPlatformConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/DigitalOceanAppPlatformConfigurePage.tsx index 61c2c5789..2bc740f05 100644 --- a/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/DigitalOceanAppPlatformConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/DigitalOceanAppPlatformConfigurePage.tsx @@ -17,6 +17,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const DigitalOceanAppPlatformConfigurePage = () => { const navigate = useNavigate(); @@ -74,6 +75,9 @@ export const DigitalOceanAppPlatformConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/route.tsx index 80eaf0bae..505206b71 100644 --- a/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/DigitalOceanAppPlatformConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { DigitalOceanAppPlatformConfigurePage } from "./DigitalOceanAppPlatformConfigurePage"; const DigitalOceanAppPlatformConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/FlyioAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/FlyioAuthorizePage/route.tsx index 44e78a708..3393aa029 100644 --- a/frontend/src/pages/secret-manager/integrations/FlyioAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/FlyioAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { FlyioAuthorizePage } from "./FlyioAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/FlyioConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/FlyioConfigurePage.tsx index c0002c553..44c81d994 100644 --- a/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/FlyioConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/FlyioConfigurePage.tsx @@ -27,6 +27,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const FlyioConfigurePage = () => { const navigate = useNavigate(); @@ -88,6 +89,9 @@ export const FlyioConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/route.tsx index 254068dbe..19b009c70 100644 --- a/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/FlyioConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { FlyioConfigurePage } from "./FlyioConfigurePage"; const FlyioConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerAuthorizePage/route.tsx index 92df323ce..6e48d2916 100644 --- a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GcpSecretManagerAuthorizePage } from "./GcpSecretManagerAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/GcpSecretManagerConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/GcpSecretManagerConfigurePage.tsx index 698ec82aa..c0cb2c171 100644 --- a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/GcpSecretManagerConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/GcpSecretManagerConfigurePage.tsx @@ -33,6 +33,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; enum TabSections { Connection = "connection", @@ -153,6 +154,9 @@ export const GcpSecretManagerConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/route.tsx index 644a9f858..cad85ec41 100644 --- a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GcpSecretManagerConfigurePage } from "./GcpSecretManagerConfigurePage"; const GcpConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerOauthCallbackPage/route.tsx index 3049d2bb6..239f583b2 100644 --- a/frontend/src/pages/secret-manager/integrations/GcpSecretManagerOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GcpSecretManagerOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GcpSecretManagerOauthCallbackPage } from "./GcpSecretManagerOauthCallbackPage"; export const GcpSecretManagerOAuthCallbackPageQueryParamsSchema = z.object({ @@ -22,7 +24,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GithubAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/GithubAuthorizePage/route.tsx index 584592ed1..248a0e328 100644 --- a/frontend/src/pages/secret-manager/integrations/GithubAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GithubAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GithubAuthorizePage } from "./GithubAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx index 01309b661..c51582075 100644 --- a/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/GithubConfigurePage.tsx @@ -45,6 +45,7 @@ import { useGetIntegrationAuthGithubEnvs, useGetIntegrationAuthGithubOrgs } from "@app/hooks/api"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; enum TabSections { Connection = "connection", @@ -269,6 +270,9 @@ export const GithubConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/route.tsx index 6bcf6db77..9ff038882 100644 --- a/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GithubConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GithubConfigurePage } from "./GithubConfigurePage"; const GithubConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GithubOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/GithubOauthCallbackPage/route.tsx index 234d43a41..bf6f7a342 100644 --- a/frontend/src/pages/secret-manager/integrations/GithubOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GithubOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GithubOauthCallbackPage } from "./GithubOauthCallbackPage"; export const GithubOAuthCallbackPageQueryParamsSchema = z.object({ @@ -23,7 +25,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GitlabAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/GitlabAuthorizePage/route.tsx index 9b132210e..e38ac0809 100644 --- a/frontend/src/pages/secret-manager/integrations/GitlabAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GitlabAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GitlabAuthorizePage } from "./GitlabAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/GitlabConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/GitlabConfigurePage.tsx index f09e13024..b88b10b03 100644 --- a/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/GitlabConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/GitlabConfigurePage.tsx @@ -35,6 +35,7 @@ import { useGetIntegrationAuthTeams } from "@app/hooks/api/integrationAuth"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const gitLabEntities = [ { name: "Individual", value: "individual" }, @@ -177,6 +178,9 @@ export const GitlabConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/route.tsx index b09ab1cf0..64d3a3a36 100644 --- a/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GitlabConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GitlabConfigurePage } from "./GitlabConfigurePage"; const GitlabConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/GitlabOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/GitlabOauthCallbackPage/route.tsx index 9a4644845..f775a13fb 100644 --- a/frontend/src/pages/secret-manager/integrations/GitlabOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/GitlabOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { GitLabOAuthCallbackPage } from "./GitlabOauthCallbackPage"; export const GitlabOAuthCallbackPageQueryParamsSchema = z.object({ @@ -22,7 +24,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/route.tsx index 02d2600c7..d6b124572 100644 --- a/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/HashicorpVaultAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { HashicorpVaultAuthorizePage } from "./HashicorpVaultAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx index c31a74371..3fbb6501f 100644 --- a/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/HashicorpVaultConfigurePage.tsx @@ -29,6 +29,7 @@ import { useWorkspace } from "@app/context"; import { isValidPath } from "@app/helpers/string"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const generateFormSchema = (availableEnvironmentNames: string[]) => { return z.object({ @@ -103,6 +104,9 @@ export const HashicorpVaultConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/route.tsx index a298b9bba..1f2c31660 100644 --- a/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/HashicorpVaultConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { HashicorpVaultConfigurePage } from "./HashicorpVaultConfigurePage"; const HashicorpVaultConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/HasuraCloudAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/HasuraCloudAuthorizePage/route.tsx index 7b9de974b..572b3a005 100644 --- a/frontend/src/pages/secret-manager/integrations/HasuraCloudAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/HasuraCloudAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { HasuraCloudAuthorizePage } from "./HasuraCloudAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/HasuraCloudConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/HasuraCloudConfigurePage.tsx index 939bd8254..06dbe61f1 100644 --- a/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/HasuraCloudConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/HasuraCloudConfigurePage.tsx @@ -15,6 +15,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const schema = z.object({ secretPath: z.string().trim(), @@ -72,6 +73,9 @@ export const HasuraCloudConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/route.tsx index d7920bb1e..c475e5bd1 100644 --- a/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/HasuraCloudConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { HasuraCloudConfigurePage } from "./HasuraCloudConfigurePage"; const HasuraCloudConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/HerokuConfigurePage/HerokuConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/HerokuConfigurePage/HerokuConfigurePage.tsx index 394c37212..79763e475 100644 --- a/frontend/src/pages/secret-manager/integrations/HerokuConfigurePage/HerokuConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/HerokuConfigurePage/HerokuConfigurePage.tsx @@ -18,6 +18,7 @@ import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const initialSyncBehaviors = [ { @@ -100,6 +101,9 @@ export const HerokuConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/HerokuConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/HerokuConfigurePage/route.tsx index a33757bc1..bcfaf2f3e 100644 --- a/frontend/src/pages/secret-manager/integrations/HerokuConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/HerokuConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { HerokuConfigurePage } from "./HerokuConfigurePage"; const HerokuConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/HerokuOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/HerokuOauthCallbackPage/route.tsx index b180e9d6a..5587e930b 100644 --- a/frontend/src/pages/secret-manager/integrations/HerokuOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/HerokuOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { HerokuOAuthCallbackPage } from "./HerokuOauthCallbackPage"; export const HerokuOAuthCallbackPageQueryParamsSchema = z.object({ @@ -22,7 +24,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/LaravelForgeAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/LaravelForgeAuthorizePage/route.tsx index 0778c8f5d..dd49bb33f 100644 --- a/frontend/src/pages/secret-manager/integrations/LaravelForgeAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/LaravelForgeAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { LaravelForgeAuthorizePage } from "./LaravelForgeAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/LaravelForgeConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/LaravelForgeConfigurePage.tsx index 2d7902c01..5c6ead598 100644 --- a/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/LaravelForgeConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/LaravelForgeConfigurePage.tsx @@ -17,6 +17,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const LaravelForgeConfigurePage = () => { const navigate = useNavigate(); @@ -74,6 +75,9 @@ export const LaravelForgeConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/route.tsx index 98d95b670..3a58ecacf 100644 --- a/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/LaravelForgeConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { LaravelForgeConfigurePage } from "./LaravelForgeConfigurePage"; const LaravelForgeConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/NetlifyConfigurePage/NetlifyConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/NetlifyConfigurePage/NetlifyConfigurePage.tsx index 44a9acbd7..99cf2b434 100644 --- a/frontend/src/pages/secret-manager/integrations/NetlifyConfigurePage/NetlifyConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/NetlifyConfigurePage/NetlifyConfigurePage.tsx @@ -17,6 +17,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const netlifyEnvironments = [ { name: "Local development", slug: "dev" }, @@ -81,6 +82,9 @@ export const NetlifyConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/NetlifyConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/NetlifyConfigurePage/route.tsx index 81f3877bd..2da84a523 100644 --- a/frontend/src/pages/secret-manager/integrations/NetlifyConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/NetlifyConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { NetlifyConfigurePage } from "./NetlifyConfigurePage"; const NetlifyConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/NetlifyOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/NetlifyOauthCallbackPage/route.tsx index d88dc29b2..37b667bf9 100644 --- a/frontend/src/pages/secret-manager/integrations/NetlifyOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/NetlifyOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { NetlifyOauthCallbackPage } from "./NetlifyOauthCallbackPage"; export const NetlifyOAuthCallbackPageQueryParamsSchema = z.object({ @@ -22,7 +24,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/NorthflankAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/NorthflankAuthorizePage/route.tsx index b0e2c10f8..1ddca538f 100644 --- a/frontend/src/pages/secret-manager/integrations/NorthflankAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/NorthflankAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { NorthflankAuthorizePage } from "./NorthflankAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/NorthflankConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/NorthflankConfigurePage.tsx index 9d99cc940..2338ea3ef 100644 --- a/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/NorthflankConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/NorthflankConfigurePage.tsx @@ -18,6 +18,7 @@ import { useGetIntegrationAuthById, useGetIntegrationAuthNorthflankSecretGroups } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const NorthflankConfigurePage = () => { const navigate = useNavigate(); @@ -95,6 +96,9 @@ export const NorthflankConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/route.tsx index fe5c2283a..1d2db4fda 100644 --- a/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/NorthflankConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { NorthflankConfigurePage } from "./NorthflankConfigurePage"; const NorthflankConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/OctopusDeployAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/OctopusDeployAuthorizePage/route.tsx index c50832858..0064e2a57 100644 --- a/frontend/src/pages/secret-manager/integrations/OctopusDeployAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/OctopusDeployAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { OctopusDeployAuthorizePage } from "./OctopusDeployAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx index 2e9d699ba..1c4315cd2 100644 --- a/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/OctopusDeployConfigurePage.tsx @@ -23,6 +23,7 @@ import { useGetIntegrationAuthOctopusDeploySpaces } from "@app/hooks/api/integrationAuth/queries"; import { OctopusDeployScope } from "@app/hooks/api/integrationAuth/types"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const formSchema = z.object({ scope: z.nativeEnum(OctopusDeployScope), @@ -87,7 +88,7 @@ export const OctopusDeployConfigurePage = () => { { integrationAuthId, spaceId: currentSpace?.Id, - resourceId: currentResource.appId!, + resourceId: currentResource?.appId || "", scope: currentScope }, { enabled: Boolean(currentSpace && currentResource) } @@ -137,6 +138,9 @@ export const OctopusDeployConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/route.tsx index 5c897a95c..264c17dcb 100644 --- a/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/OctopusDeployConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { OctopusDeployConfigurePage } from "./OctopusDeployConfigurePage"; const OctopusDeployConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/QoveryAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/QoveryAuthorizePage/route.tsx index eddbca06f..e63745397 100644 --- a/frontend/src/pages/secret-manager/integrations/QoveryAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/QoveryAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { QoveryAuthorizePage } from "./QoveryAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/QoveryConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/QoveryConfigurePage.tsx index 5041782aa..e2d294442 100644 --- a/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/QoveryConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/QoveryConfigurePage.tsx @@ -28,6 +28,7 @@ import { useGetIntegrationAuthQoveryProjects, useGetIntegrationAuthQoveryScopes } from "@app/hooks/api/integrationAuth/queries"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const qoveryScopes = [ { label: "Application", value: "application" }, @@ -178,6 +179,9 @@ export const QoveryConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/route.tsx index 1f14f02ca..ae6fb36fe 100644 --- a/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/QoveryConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { QoveryConfigurePage } from "./QoveryConfigurePage"; const QoveryConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/RailwayAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/RailwayAuthorizePage/route.tsx index d351b5f8f..8d66fbfc0 100644 --- a/frontend/src/pages/secret-manager/integrations/RailwayAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/RailwayAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { RailwayAuthorizePage } from "./RailwayAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/RailwayConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/RailwayConfigurePage.tsx index 9b4162072..ea3894203 100644 --- a/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/RailwayConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/RailwayConfigurePage.tsx @@ -19,6 +19,7 @@ import { useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const RailwayConfigurePage = () => { const navigate = useNavigate(); @@ -110,6 +111,9 @@ export const RailwayConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/route.tsx index 411c960a5..aa7c9070f 100644 --- a/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/RailwayConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { RailwayConfigurePage } from "./RailwayConfigurePage"; const RailwayConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/RenderAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/RenderAuthorizePage/route.tsx index 3657f2a07..f8f25e764 100644 --- a/frontend/src/pages/secret-manager/integrations/RenderAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/RenderAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { RenderAuthorizePage } from "./RenderAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/RenderConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/RenderConfigurePage.tsx index 40c00d209..f30ff6b14 100644 --- a/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/RenderConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/RenderConfigurePage.tsx @@ -29,6 +29,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const schema = z.object({ selectedSourceEnvironment: z.string(), @@ -106,6 +107,9 @@ export const RenderConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/route.tsx index 8c676c800..59486fa96 100644 --- a/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/RenderConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { RenderConfigurePage } from "./RenderConfigurePage"; const RenderConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/RundeckAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/RundeckAuthorizePage/route.tsx index 3ff55ddb4..a878544b8 100644 --- a/frontend/src/pages/secret-manager/integrations/RundeckAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/RundeckAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { RundeckAuthorizePage } from "./RundeckAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/RundeckConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/RundeckConfigurePage.tsx index 10b16c6ea..9b49bd8ec 100644 --- a/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/RundeckConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/RundeckConfigurePage.tsx @@ -20,6 +20,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { useWorkspace } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const schema = z.object({ keyStoragePath: z.string().trim().min(1, { message: "Rundeck Key Storage path is required" }), @@ -73,6 +74,9 @@ export const RundeckConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/route.tsx index 3683b0fc0..866778387 100644 --- a/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/RundeckConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { RundeckConfigurePage } from "./RundeckConfigurePage"; const RundeskConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/SelectIntegrationAuthPage/route.tsx b/frontend/src/pages/secret-manager/integrations/SelectIntegrationAuthPage/route.tsx index 26eb213aa..1e7ce337c 100644 --- a/frontend/src/pages/secret-manager/integrations/SelectIntegrationAuthPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/SelectIntegrationAuthPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { SelectIntegrationAuthPage } from "./SelectIntegrationAuthPage"; const SelectIntegrationAuthPageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/SupabaseAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/SupabaseAuthorizePage/route.tsx index 544867b50..a3b7a826e 100644 --- a/frontend/src/pages/secret-manager/integrations/SupabaseAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/SupabaseAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { SupabaseAuthorizePage } from "./SupabaseAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/SupabaseConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/SupabaseConfigurePage.tsx index 3cb604935..8c2aa0a09 100644 --- a/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/SupabaseConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/SupabaseConfigurePage.tsx @@ -17,6 +17,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const SupabaseConfigurePage = () => { const navigate = useNavigate(); @@ -73,6 +74,9 @@ export const SupabaseConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/route.tsx index dc2f73eee..e5d50d350 100644 --- a/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/SupabaseConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { SupabaseConfigurePage } from "./SupabaseConfigurePage"; const SupabaseConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/TeamcityAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/TeamcityAuthorizePage/route.tsx index 9983c9b9c..e9bc5534b 100644 --- a/frontend/src/pages/secret-manager/integrations/TeamcityAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/TeamcityAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { TeamcityAuthorizePage } from "./TeamcityAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/TeamcityConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/TeamcityConfigurePage.tsx index 24fda3be4..96770c19f 100644 --- a/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/TeamcityConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/TeamcityConfigurePage.tsx @@ -21,6 +21,7 @@ import { useGetIntegrationAuthById, useGetIntegrationAuthTeamCityBuildConfigs } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const TeamcityConfigurePage = () => { const navigate = useNavigate(); @@ -92,6 +93,9 @@ export const TeamcityConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/route.tsx index 57301a69c..73532c919 100644 --- a/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/TeamcityConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { TeamcityConfigurePage } from "./TeamcityConfigurePage"; const TeamcityConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/TerraformCloudAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/TerraformCloudAuthorizePage/route.tsx index d15f50f2c..ad936b182 100644 --- a/frontend/src/pages/secret-manager/integrations/TerraformCloudAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/TerraformCloudAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { TerraformCloudAuthorizePage } from "./TerraformCloudAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/TerraformCloudConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/TerraformCloudConfigurePage.tsx index 97f6bd166..a247144fa 100644 --- a/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/TerraformCloudConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/TerraformCloudConfigurePage.tsx @@ -21,6 +21,7 @@ import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const initialSyncBehaviors = [ { @@ -107,6 +108,9 @@ export const TerraformCloudConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/route.tsx index 373e10839..ad2a48182 100644 --- a/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/TerraformCloudConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { TerraformCloudConfigurePage } from "./TerraformCloudConfigurePage"; const TerraformCloudConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/TravisCIAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/TravisCIAuthorizePage/route.tsx index 71e5c123b..eaa138804 100644 --- a/frontend/src/pages/secret-manager/integrations/TravisCIAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/TravisCIAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { TravisCIAuthorizePage } from "./TravisCIAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/TravisCIConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/TravisCIConfigurePage.tsx index b6fff7fef..807a6a87e 100644 --- a/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/TravisCIConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/TravisCIConfigurePage.tsx @@ -17,6 +17,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const TravisCIConfigurePage = () => { const navigate = useNavigate(); @@ -74,6 +75,9 @@ export const TravisCIConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/route.tsx index ecc256de4..cc5600ffd 100644 --- a/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/TravisCIConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { TravisCIConfigurePage } from "./TravisCIConfigurePage"; const TravisCIConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/VercelConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/VercelConfigurePage.tsx index fabccb6bb..4a421274c 100644 --- a/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/VercelConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/VercelConfigurePage.tsx @@ -29,6 +29,7 @@ import { useGetIntegrationAuthVercelCustomEnvironments } from "@app/hooks/api/integrationAuth"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; const vercelEnvironments = [ { name: "Development", slug: "development" }, @@ -134,6 +135,9 @@ export const VercelConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/route.tsx index 4b743ceb1..3f36ba5fa 100644 --- a/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { VercelConfigurePage } from "./VercelConfigurePage"; const VercelConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/VercelOauthCallbackPage/route.tsx b/frontend/src/pages/secret-manager/integrations/VercelOauthCallbackPage/route.tsx index fcdf72976..6bfa5dac8 100644 --- a/frontend/src/pages/secret-manager/integrations/VercelOauthCallbackPage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/VercelOauthCallbackPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { VercelOauthCallbackPage } from "./VercelOauthCallbackPage"; export const VercelOAuthCallbackPageQueryParamsSchema = z.object({ @@ -22,7 +24,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/route.tsx b/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/route.tsx index b81b4a7fd..1985bef00 100644 --- a/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/route.tsx @@ -1,5 +1,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { WindmillAuthorizePage } from "./WindmillAuthorizePage"; export const Route = createFileRoute( @@ -14,7 +16,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/WindmillConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/WindmillConfigurePage.tsx index 1d8094059..ad70c75ba 100644 --- a/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/WindmillConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/WindmillConfigurePage.tsx @@ -17,6 +17,7 @@ import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; export const WindmillConfigurePage = () => { const navigate = useNavigate(); @@ -75,6 +76,9 @@ export const WindmillConfigurePage = () => { to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.id + }, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } catch (err) { diff --git a/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/route.tsx b/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/route.tsx index 4ff5443b2..cc3442340 100644 --- a/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/route.tsx +++ b/frontend/src/pages/secret-manager/integrations/WindmillConfigurePage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; + import { WindmillConfigurePage } from "./WindmillConfigurePage"; const WindmillConfigurePageQueryParamsSchema = z.object({ @@ -21,7 +23,10 @@ export const Route = createFileRoute( label: "Integrations", link: linkOptions({ to: "/secret-manager/$projectId/integrations", - params + params, + search: { + selectedTab: IntegrationsListPageTabs.NativeIntegrations + } }) }, { diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index c77152caf..60d971403 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -60,6 +60,7 @@ import { Route as organizationKmsOverviewPageRouteImport } from './pages/organiz import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route' import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route' import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route' +import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route' import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh' import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager' import { Route as projectAccessControlPageRouteKmsImport } from './pages/project/AccessControlPage/route-kms' @@ -102,7 +103,7 @@ import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/ import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route' import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route' import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route' -import { Route as organizationAppConnectionsGithubOauthCallbackPageRouteImport } from './pages/organization/AppConnections/GithubOauthCallbackPage/route' +import { Route as organizationAppConnectionsOauthCallbackPageRouteImport } from './pages/organization/AppConnections/OauthCallbackPage/route' import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route' import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route' import { Route as secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route' @@ -208,6 +209,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport = createFileRoute( '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId', )() +const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport = + createFileRoute( + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections', + )() const AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdImport = createFileRoute( '/_authenticate/_inject-org-details/_org-layout/kms/$projectId', )() @@ -441,6 +446,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute = getParentRoute: () => organizationLayoutRoute, } as any) +const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute = + AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({ + id: '/app-connections', + path: '/app-connections', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, + } as any) + const AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRoute = AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdImport.update({ id: '/kms/$projectId', @@ -613,6 +626,14 @@ const organizationCertManagerOverviewPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) +const organizationAppConnectionsAppConnectionsPageRouteRoute = + organizationAppConnectionsAppConnectionsPageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute, + } as any) + const projectAccessControlPageRouteSshRoute = projectAccessControlPageRouteSshImport.update({ id: '/access-management', @@ -923,12 +944,12 @@ const secretManagerIntegrationsDetailsByIDPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute, } as any) -const organizationAppConnectionsGithubOauthCallbackPageRouteRoute = - organizationAppConnectionsGithubOauthCallbackPageRouteImport.update({ - id: '/app-connections/github/oauth/callback', - path: '/app-connections/github/oauth/callback', +const organizationAppConnectionsOauthCallbackPageRouteRoute = + organizationAppConnectionsOauthCallbackPageRouteImport.update({ + id: '/$appConnection/oauth/callback', + path: '/$appConnection/oauth/callback', getParentRoute: () => - AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, + AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute, } as any) const certManagerCertAuthDetailsByIDPageRouteRoute = @@ -1859,6 +1880,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdImport parentRoute: typeof organizationLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections' + path: '/app-connections' + fullPath: '/organization/app-connections' + preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': { id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId' path: '/secret-manager/$projectId' @@ -1873,6 +1901,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdImport parentRoute: typeof organizationLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/' + path: '/' + fullPath: '/organization/app-connections/' + preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport + } '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': { id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview' path: '/cert-manager/overview' @@ -2153,12 +2188,12 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport parentRoute: typeof certManagerLayoutImport } - '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback' - path: '/app-connections/github/oauth/callback' - fullPath: '/organization/app-connections/github/oauth/callback' - preLoaderRoute: typeof organizationAppConnectionsGithubOauthCallbackPageRouteImport - parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' + path: '/$appConnection/oauth/callback' + fullPath: '/organization/app-connections/$appConnection/oauth/callback' + preLoaderRoute: typeof organizationAppConnectionsOauthCallbackPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': { id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId' @@ -2848,6 +2883,24 @@ const AuthenticateInjectOrgDetailsOrgLayoutIntegrationsRouteWithChildren = AuthenticateInjectOrgDetailsOrgLayoutIntegrationsRouteChildren, ) +interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren { + organizationAppConnectionsAppConnectionsPageRouteRoute: typeof organizationAppConnectionsAppConnectionsPageRouteRoute + organizationAppConnectionsOauthCallbackPageRouteRoute: typeof organizationAppConnectionsOauthCallbackPageRouteRoute +} + +const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren = + { + organizationAppConnectionsAppConnectionsPageRouteRoute: + organizationAppConnectionsAppConnectionsPageRouteRoute, + organizationAppConnectionsOauthCallbackPageRouteRoute: + organizationAppConnectionsOauthCallbackPageRouteRoute, + } + +const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren = + AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute._addFileChildren( + AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren, + ) + interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute @@ -2857,6 +2910,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute + AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute @@ -2865,7 +2919,6 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationRoleByIDPageRouteRoute: typeof organizationRoleByIDPageRouteRoute organizationSecretManagerOverviewPageRouteRoute: typeof organizationSecretManagerOverviewPageRouteRoute organizationSshOverviewPageRouteRoute: typeof organizationSshOverviewPageRouteRoute - organizationAppConnectionsGithubOauthCallbackPageRouteRoute: typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren = @@ -2881,6 +2934,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationSecretSharingPageRouteRoute: organizationSecretSharingPageRouteRoute, organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute, + AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: + AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren, organizationCertManagerOverviewPageRouteRoute: organizationCertManagerOverviewPageRouteRoute, organizationGroupDetailsByIDPageRouteRoute: @@ -2896,8 +2951,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationSecretManagerOverviewPageRouteRoute, organizationSshOverviewPageRouteRoute: organizationSshOverviewPageRouteRoute, - organizationAppConnectionsGithubOauthCallbackPageRouteRoute: - organizationAppConnectionsGithubOauthCallbackPageRouteRoute, } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren = @@ -3555,8 +3608,10 @@ export interface FileRoutesByFullPath { '/organization/settings': typeof organizationSettingsPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren + '/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren '/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren '/ssh/$projectId': typeof sshLayoutRouteWithChildren + '/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute @@ -3593,7 +3648,7 @@ export interface FileRoutesByFullPath { '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute - '/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute + '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute '/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute @@ -3725,6 +3780,7 @@ export interface FileRoutesByTo { '/kms/$projectId': typeof kmsLayoutRouteWithChildren '/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren '/ssh/$projectId': typeof sshLayoutRouteWithChildren + '/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute @@ -3760,7 +3816,7 @@ export interface FileRoutesByTo { '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute - '/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute + '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute '/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute @@ -3900,8 +3956,10 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof organizationSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute @@ -3942,7 +4000,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute @@ -4078,8 +4136,10 @@ export interface FileRouteTypes { | '/organization/settings' | '/cert-manager/$projectId' | '/kms/$projectId' + | '/organization/app-connections' | '/secret-manager/$projectId' | '/ssh/$projectId' + | '/organization/app-connections/' | '/organization/cert-manager/overview' | '/organization/groups/$groupId' | '/organization/identities/$identityId' @@ -4116,7 +4176,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/access-management' | '/secret-manager/$projectId/integrations/' | '/cert-manager/$projectId/ca/$caId' - | '/organization/app-connections/github/oauth/callback' + | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' | '/secret-manager/$projectId/secrets/$envSlug' @@ -4247,6 +4307,7 @@ export interface FileRouteTypes { | '/kms/$projectId' | '/secret-manager/$projectId' | '/ssh/$projectId' + | '/organization/app-connections' | '/organization/cert-manager/overview' | '/organization/groups/$groupId' | '/organization/identities/$identityId' @@ -4282,7 +4343,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/access-management' | '/secret-manager/$projectId/integrations' | '/cert-manager/$projectId/ca/$caId' - | '/organization/app-connections/github/oauth/callback' + | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' | '/secret-manager/$projectId/secrets/$envSlug' @@ -4420,8 +4481,10 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/settings' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId' + | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId' + | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/' | '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview' | '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId' @@ -4462,7 +4525,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId' - | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback' + | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug' @@ -4781,6 +4844,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/settings", + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections", "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview", "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId", @@ -4788,8 +4852,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId", "/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview", - "/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview", - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback" + "/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview" ] }, "/_authenticate/_inject-org-details/admin/_admin-layout": { @@ -4849,6 +4912,14 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" ] }, + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections": { + "filePath": "", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization", + "children": [ + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/", + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" + ] + }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout", @@ -4863,6 +4934,10 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" ] }, + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/": { + "filePath": "organization/AppConnections/AppConnectionsPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections" + }, "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": { "filePath": "organization/CertManagerOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" @@ -5144,9 +5219,9 @@ export const routeTree = rootRoute "filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback": { - "filePath": "organization/AppConnections/GithubOauthCallbackPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/_org-layout/organization" + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback": { + "filePath": "organization/AppConnections/OauthCallbackPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections" }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId": { "filePath": "secret-manager/IntegrationsDetailsByIDPage/route.tsx", diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index b81b13165..e42de96d6 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -24,10 +24,13 @@ const organizationRoutes = route("/organization", [ route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"), route("/roles/$roleId", "organization/RoleByIDPage/route.tsx"), route("/identities/$identityId", "organization/IdentityDetailsByIDPage/route.tsx"), - route( - "/app-connections/github/oauth/callback", - "organization/AppConnections/GithubOauthCallbackPage/route.tsx" - ) + route("/app-connections", [ + index("organization/AppConnections/AppConnectionsPage/route.tsx"), + route( + "/$appConnection/oauth/callback", + "organization/AppConnections/OauthCallbackPage/route.tsx" + ) + ]) ]); const secretManagerRoutes = route("/secret-manager/$projectId", [