1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-29 22:02:57 +00:00

Compare commits

..

98 Commits

Author SHA1 Message Date
db7a6f3530 fix: remove deletion of other secrets from many-to-one mapping 2025-02-20 12:21:52 -08:00
d80a104f7b Merge pull request from Infisical/feat/kmip-client-management
feat: kmip
2025-02-20 17:53:15 +08:00
d980d471e8 Merge pull request from Infisical/doc/add-caching-reference-to-go-sdk
doc: add caching reference for go sdk
2025-02-19 22:00:27 -05:00
3583a09ab5 Merge pull request from Infisical/fix-org-select-none
Fix failing redirect to create new organization page on no organizations
2025-02-19 13:53:33 -08:00
86b6d23f34 Fix lint issue 2025-02-19 10:27:55 -08:00
2c31ac0569 misc: finalized KMIP icon 2025-02-20 02:11:04 +08:00
d6c1b8e30c misc: addressed comments 2025-02-20 01:46:50 +08:00
0d4d73b61d misc: update default usage to be numerical 2025-02-19 20:16:45 +08:00
198b607e2e doc: add caching reference for go sdk 2025-02-19 20:11:14 +08:00
f0e6bcef9b fix: addressed rabbit findings 2025-02-19 17:16:02 +08:00
3a5a88467d Merge remote-tracking branch 'origin' into fix-org-select-none 2025-02-18 18:55:08 -08:00
012f265363 Fix lint issues 2025-02-18 18:42:39 -08:00
823bf134dd Fix case where user can get stuck in a deleted organization if they were prev logged into it 2025-02-18 18:35:22 -08:00
1f5a73047d Merge pull request from Infisical/doc/added-docs-for-egress-ips
doc: added section for egress ips
2025-02-18 19:04:03 -05:00
0366df6e19 Update terraform-cloud.mdx 2025-02-18 17:48:56 -05:00
c77e0c0666 Merge pull request from Infisical/oidc-guide-tf-cloud
OIDC Guide for Terraform cloud <> Infisical
2025-02-18 17:27:15 -05:00
8e70731c4c add terraform cloud oidc docs 2025-02-18 17:23:03 -05:00
1db8c9ea29 doc: finish up KMIP 2025-02-19 02:59:04 +08:00
21c6700db2 Merge pull request from thomas-infisical/terraform-provider
docs: add ephemeral resources documentation to Terraform guide
2025-02-18 09:55:42 -08:00
619062033b docs: enhance Terraform integration guide with ephemeral resources and best practices 2025-02-18 17:40:24 +01:00
92b3b9157a feat: added kmip server to CLI 2025-02-18 20:59:14 +08:00
f6cd78e078 misc: added exception for kmip 2025-02-18 17:41:34 +08:00
36973b1b5c Merge pull request from akhilmhdh/feat/batch-upsert
Batch upsert operation
2025-02-18 09:32:01 +05:30
=
1ca578ee03 feat: updated based on review feedback 2025-02-18 00:45:53 +05:30
=
8a7f7ac9fd feat: added test cases for bulk update 2025-02-18 00:23:10 +05:30
=
049fd8e769 feat: added upsert and ignore to bulk update 2025-02-18 00:23:10 +05:30
8e2cce865a Merge remote-tracking branch 'origin/main' into feat/kmip-client-management 2025-02-18 02:50:12 +08:00
943c2b0e69 misc: finalize KMIP management 2025-02-18 02:22:59 +08:00
8518fed726 Merge remote-tracking branch 'origin' into fix-org-select-none 2025-02-17 09:20:29 -08:00
5148435f5f Move user to create org screen on no org 2025-02-17 09:20:19 -08:00
2c825616a6 Merge pull request from Infisical/update-hardware
Update hardware for infisical
2025-02-17 09:55:35 -05:00
febbd4ade5 update hardware for infisical 2025-02-17 09:51:30 -05:00
603b740bbe misc: migrated KMIP PKI to be scoped at the org level 2025-02-17 15:54:02 +08:00
874dc01692 Update upgrading-infisical.mdx 2025-02-14 23:43:15 -05:00
b44b8bf647 Merge pull request from Infisical/upgrade-infisical-dcos
Docs for new upgrade process
2025-02-14 20:41:41 -05:00
258e561b84 add upgrade docs 2025-02-14 20:41:05 -05:00
5802638fc4 Merge pull request from Infisical/is-pending-fixes
Improvement: Use isPending Over isLoading
2025-02-14 09:10:05 -08:00
e2e7004583 improvement: additional isPending fix 2025-02-14 08:59:12 -08:00
7826324435 fix: use isPending over isLoading 2025-02-14 08:54:37 -08:00
af652f7e52 feat: kmip poc done 2025-02-14 23:55:57 +08:00
2bfc1caec5 Merge pull request from Infisical/fix-org-options-alignment
Improvement: Align Organization Options in Sidebar
2025-02-13 16:30:02 -08:00
4b9e3e44e2 improvement: align organization options in sidebar 2025-02-13 16:25:59 -08:00
b2a680ebd7 Merge pull request from thomas-infisical/docs-components
docs: reorganize components documentation
2025-02-13 12:18:36 -08:00
b269bb81fe Merge pull request from Infisical/fix-check-permissions-on-before-load-integrations
Fix (temp): Wrap Integrations beforeLoad in Try/Catch and RenderBannerGuard if order
2025-02-13 08:27:59 -08:00
5ca7ff4f2d refactor: reorganize components documentation for clarity and structure 2025-02-13 16:35:19 +01:00
ec12d57862 fix: refine try/catch 2025-02-12 20:56:07 -08:00
2d16f5f258 fix (temp): wrap integrations before load in try/catch, fix render banner guard 2025-02-12 20:35:00 -08:00
93912da528 Merge pull request from Infisical/daniel/fix-octopus-integration
fix: octopus deploy integration error
2025-02-13 08:02:18 +04:00
ffc5e61faa Update OctopusDeployConfigurePage.tsx 2025-02-13 07:56:24 +04:00
70e68f4441 Merge pull request from Infisical/maidul-adidsd
Update auto migratiom msg
2025-02-12 19:21:09 -05:00
a004934a28 update auto migration msg 2025-02-12 19:20:24 -05:00
0811192eed Merge pull request from Infisical/daniel/delete-integration
fix(native-integrations): delete integrations from details page
2025-02-13 04:01:38 +04:00
1e09487572 Merge pull request from Infisical/snyk-fix-85b2bc501b20e7ef8b4f85e965b21c49
[Snyk] Fix for 2 vulnerabilities
2025-02-12 19:01:09 -05:00
86202caa95 Merge pull request from Infisical/snyk-fix-dc81ef6fa0253f665c563233d6aa0a54
[Snyk] Security upgrade @fastify/multipart from 8.3.0 to 8.3.1
2025-02-12 19:00:32 -05:00
285fca4ded Merge pull request from Infisical/databricks-connection-and-sync
Feature: Databricks Connection & Sync
2025-02-12 12:16:41 -08:00
30fb60b441 resolve merge 2025-02-12 11:11:11 -08:00
e531390922 improvement: address feedback 2025-02-12 10:39:13 -08:00
e88ce49463 Delete .github/workflows/deployment-pipeline.yml 2025-02-12 13:10:07 -05:00
9214c93ece Merge pull request from Infisical/minor-ui-improvements
Improvement: UI Improvements & Invite User to Org from Project Invite Modal
2025-02-12 10:07:42 -08:00
7a3bfa9e4c improve query 2025-02-12 17:34:53 +00:00
7aa0e8572c Merge pull request from Infisical/daniel/minor-improvements
fix: minor improvements
2025-02-12 20:37:37 +04:00
296efa975c chore: fix lint 2025-02-12 20:33:13 +04:00
b3e72c338f Update group-dal.ts 2025-02-12 20:26:19 +04:00
8c4c969bc2 Merge pull request from Infisical/revert-3103-revert-3102-feat/enc-migration
Revert "Revert "Feat/enc migration""
2025-02-11 23:47:54 -05:00
0d424f332a Merge pull request from Infisical/revert-3104-revert-2827-feat/enc-migration
Revert "Revert "Root encrypted data to kms encryption""
2025-02-11 23:47:45 -05:00
f0b6382f92 Revert "Revert "Root encrypted data to kms encryption"" 2025-02-11 22:41:32 -05:00
72780c61b4 fix: check create member permission for invite ability 2025-02-11 16:37:25 -08:00
c4da0305ba improvement: supress eslint error and improve text 2025-02-11 16:19:37 -08:00
4fdfdc1a39 improvements: ui improvements & add users to org from project member invite modal 2025-02-11 16:14:13 -08:00
d2cf296250 Merge pull request from Infisical/daniel/fix-arn-2
fix: arn regex validation
2025-02-11 21:00:54 +01:00
30284e3458 Update identity-aws-auth-validators.ts 2025-02-11 23:58:20 +04:00
b8a07979c3 misc: audit logs 2025-02-12 01:34:29 +08:00
6f90d3befd Merge pull request from Infisical/daniel/fix-secret-key
fix(api): disallow colon in secret name & allow updating malformed secret name
2025-02-11 18:16:03 +01:00
f44888afa2 Update secret-router.ts 2025-02-11 21:08:20 +04:00
9877b0e5c4 Merge pull request from thomas-infisical/changelog-january
Update changelog with January 2025 entries
2025-02-11 08:58:31 -08:00
b1e35d4b27 Merge pull request from Infisical/revert-2827-feat/enc-migration
Revert "Root encrypted data to kms encryption"
2025-02-11 11:16:46 -05:00
25f6947de5 Revert "Root encrypted data to kms encryption" 2025-02-11 11:16:30 -05:00
ff8f1d3bfb Revert "Revert "Feat/enc migration"" 2025-02-11 11:16:09 -05:00
b1b4cb1823 Merge pull request from Infisical/revert-3102-feat/enc-migration
Revert "Feat/enc migration"
2025-02-11 11:15:59 -05:00
292c9051bd feat: kmip create and get 2025-02-11 23:32:11 +08:00
4de8888843 feature: databricks sync 2025-02-10 18:38:27 -08:00
da35ec90bc fix(native-integrations): delete integrations from details page 2025-02-11 05:18:14 +04:00
f27d483be0 Update changelog 2025-02-07 18:12:58 +01:00
9ee9d1c0e7 fixes 2025-02-07 08:57:30 +01:00
77fac45df1 misc: reordered migration 2025-02-07 01:39:48 +08:00
0ab90383c2 Merge remote-tracking branch 'origin/main' into feat/kmip-client-management 2025-02-07 01:26:04 +08:00
a3acfa65a2 feat: finished up client cert generation 2025-02-07 01:24:32 +08:00
d9a0cf8dd5 Update changelog with January 2025 entries 2025-02-06 17:43:30 +01:00
0269f57768 feat: completed kmip server cert config 2025-02-06 22:36:31 +08:00
9f9ded5102 misc: initial instance KMIP PKI setup 2025-02-06 02:57:40 +08:00
8b315c946c feat: added kmip to project roles section 2025-02-04 19:29:34 +08:00
dd9a7755bc feat: completed KMIP client overview 2025-02-04 18:49:48 +08:00
64c2fba350 feat: added list support and overview page 2025-02-04 02:44:59 +08:00
c7f80f7d9e feat: kmip client backend setup 2025-02-04 01:34:30 +08:00
ecf2cb6e51 misc: made improvements to wording 2025-01-30 13:09:29 +08:00
1e5a9a6020 doc: added section for egress ips 2025-01-30 03:36:46 +08:00
00e69e6632 fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-FASTIFYMULTIPART-8660811
2025-01-29 04:35:48 +00:00
cedb22a39a fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NANOID-8492085
- https://snyk.io/vuln/SNYK-JS-AXIOS-6671926
2024-12-12 00:58:08 +00:00
382 changed files with 9241 additions and 1665 deletions
.github/workflows
backend
e2e-test/routes/v3
package-lock.jsonpackage.json
src
@types
db
ee
lib
server/routes
services
cli
docs
api-reference/endpoints
changelog
contributing/platform/backend
documentation
platform
identities/oidc-auth
kms
setup
images
integrations
internals
mint.json
sdks/languages
self-hosting
frontend
public
src
components
context
OrgPermissionContext
ProjectPermissionContext
index.tsx
helpers
hooks/api
layouts
OrganizationLayout
OrganizationLayout.tsx
components/MinimizedOrgSidebar
ProjectLayout
pages
admin/OverviewPage
auth
LoginPage
SignUpInvitePage
kms/KmipPage
middlewares
organization
project
AccessControlPage/components/MembersTab/components
RoleDetailsBySlugPage/components
secret-manager
IntegrationsDetailsByIDPage
IntegrationsListPage
SecretSyncDetailsByIDPage
integrations
AwsParameterStoreAuthorizePage
AwsParameterStoreConfigurePage
AwsSecretManagerAuthorizePage
AwsSecretManagerConfigurePage
AzureAppConfigurationConfigurePage
AzureAppConfigurationOauthCallbackPage
AzureDevopsAuthorizePage
AzureDevopsConfigurePage
AzureKeyVaultAuthorizePage
AzureKeyVaultConfigurePage
AzureKeyVaultOauthCallbackPage
BitbucketConfigurePage
BitbucketOauthCallbackPage
ChecklyAuthorizePage
ChecklyConfigurePage
CircleCIAuthorizePage
CircleCIConfigurePage
Cloud66AuthorizePage
Cloud66ConfigurePage
CloudflarePagesAuthorizePage
CloudflarePagesConfigurePage
CloudflareWorkersAuthorizePage
CloudflareWorkersConfigurePage
CodefreshAuthorizePage
CodefreshConfigurePage
DatabricksAuthorizePage
DatabricksConfigurePage
DigitalOceanAppPlatformAuthorizePage
DigitalOceanAppPlatformConfigurePage
FlyioAuthorizePage
FlyioConfigurePage
GcpSecretManagerAuthorizePage
GcpSecretManagerConfigurePage
GcpSecretManagerOauthCallbackPage
GithubAuthorizePage
GithubConfigurePage
GithubOauthCallbackPage
GitlabAuthorizePage
GitlabConfigurePage
GitlabOauthCallbackPage
HashicorpVaultAuthorizePage
HashicorpVaultConfigurePage
HasuraCloudAuthorizePage
HasuraCloudConfigurePage
HerokuConfigurePage
HerokuOauthCallbackPage
LaravelForgeAuthorizePage
LaravelForgeConfigurePage
NetlifyConfigurePage
NetlifyOauthCallbackPage
NorthflankAuthorizePage
NorthflankConfigurePage
OctopusDeployAuthorizePage
OctopusDeployConfigurePage
QoveryAuthorizePage
QoveryConfigurePage
RailwayAuthorizePage
RailwayConfigurePage
RenderAuthorizePage
RenderConfigurePage
RundeckAuthorizePage
RundeckConfigurePage
SelectIntegrationAuthPage
SupabaseAuthorizePage
SupabaseConfigurePage
TeamcityAuthorizePage
TeamcityConfigurePage
TerraformCloudAuthorizePage
TerraformCloudConfigurePage
TravisCIAuthorizePage
TravisCIConfigurePage
VercelConfigurePage
VercelOauthCallbackPage
WindmillAuthorizePage
WindmillConfigurePage
routeTree.gen.tsroutes.ts

@ -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<https://github.com/Infisical/infisical/commit/${{ steps.commit.outputs.short }}>"
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<https://github.com/Infisical/infisical/commit/${{ steps.commit.outputs.short }}>"

@ -535,6 +535,107 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }]
);
});
test.each(secretTestCases)("Bulk upsert secrets in path $path", async ({ secret, path }) => {
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
mode: "upsert",
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: secret.comment
}))
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
);
});
test("Bulk upsert secrets in path multiple paths", async () => {
const firstBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-KEY-${secretTestCases[0].secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: "comment",
secretPath: secretTestCases[0].path
}));
const secondBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-KEY-${secretTestCases[1].secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: "comment",
secretPath: secretTestCases[1].path
}));
const testSecrets = [...firstBatchSecrets, ...secondBatchSecrets];
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
mode: "upsert",
secrets: testSecrets
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const firstBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[0].path);
expect(firstBatchSecretsOnInfisical).toEqual(
expect.arrayContaining(
firstBatchSecrets.map((el) =>
expect.objectContaining({
secretKey: el.secretKey,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
const secondBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[1].path);
expect(secondBatchSecretsOnInfisical).toEqual(
expect.arrayContaining(
secondBatchSecrets.map((el) =>
expect.objectContaining({
secretKey: el.secretKey,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(testSecrets.map((el) => deleteSecret({ path: el.secretPath, key: el.secretKey })));
});
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))

@ -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"
},

@ -45,7 +45,7 @@
"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.mjs --client pg migrate:latest",
"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",
@ -62,7 +62,7 @@
"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",

@ -16,6 +16,9 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
@ -126,6 +129,11 @@ declare module "fastify" {
isUserCompleted: string;
providerAuthToken: string;
};
kmipUser: {
projectId: string;
clientId: string;
name: string;
};
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
@ -218,11 +226,14 @@ declare module "fastify" {
totp: TTotpServiceFactory;
appConnection: TAppConnectionServiceFactory;
secretSync: TSecretSyncServiceFactory;
kmip: TKmipServiceFactory;
kmipOperation: TKmipOperationServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
store: {
user: Pick<TUserDALFactory, "findById">;
kmipClient: Pick<TKmipClientDALFactory, "findByProjectAndClientId">;
};
}
}

@ -143,6 +143,18 @@ import {
TInternalKms,
TInternalKmsInsert,
TInternalKmsUpdate,
TKmipClientCertificates,
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate,
TKmipClients,
TKmipClientsInsert,
TKmipClientsUpdate,
TKmipOrgConfigs,
TKmipOrgConfigsInsert,
TKmipOrgConfigsUpdate,
TKmipOrgServerCertificates,
TKmipOrgServerCertificatesInsert,
TKmipOrgServerCertificatesUpdate,
TKmsKeys,
TKmsKeysInsert,
TKmsKeysUpdate,
@ -902,5 +914,21 @@ declare module "knex/types/tables" {
TAppConnectionsUpdate
>;
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
[TableName.KmipClient]: KnexOriginal.CompositeTableType<TKmipClients, TKmipClientsInsert, TKmipClientsUpdate>;
[TableName.KmipOrgConfig]: KnexOriginal.CompositeTableType<
TKmipOrgConfigs,
TKmipOrgConfigsInsert,
TKmipOrgConfigsUpdate
>;
[TableName.KmipOrgServerCertificates]: KnexOriginal.CompositeTableType<
TKmipOrgServerCertificates,
TKmipOrgServerCertificatesInsert,
TKmipOrgServerCertificatesUpdate
>;
[TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType<
TKmipClientCertificates,
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate
>;
}
}

@ -0,0 +1,108 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
if (!hasKmipClientTable) {
await knex.schema.createTable(TableName.KmipClient, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.specificType("permissions", "text[]");
t.string("description");
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
}
const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig);
if (!hasKmipOrgPkiConfig) {
await knex.schema.createTable(TableName.KmipOrgConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.unique("orgId");
t.string("caKeyAlgorithm").notNullable();
t.datetime("rootCaIssuedAt").notNullable();
t.datetime("rootCaExpiration").notNullable();
t.string("rootCaSerialNumber").notNullable();
t.binary("encryptedRootCaCertificate").notNullable();
t.binary("encryptedRootCaPrivateKey").notNullable();
t.datetime("serverIntermediateCaIssuedAt").notNullable();
t.datetime("serverIntermediateCaExpiration").notNullable();
t.string("serverIntermediateCaSerialNumber");
t.binary("encryptedServerIntermediateCaCertificate").notNullable();
t.binary("encryptedServerIntermediateCaChain").notNullable();
t.binary("encryptedServerIntermediateCaPrivateKey").notNullable();
t.datetime("clientIntermediateCaIssuedAt").notNullable();
t.datetime("clientIntermediateCaExpiration").notNullable();
t.string("clientIntermediateCaSerialNumber").notNullable();
t.binary("encryptedClientIntermediateCaCertificate").notNullable();
t.binary("encryptedClientIntermediateCaChain").notNullable();
t.binary("encryptedClientIntermediateCaPrivateKey").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.KmipOrgConfig);
}
const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates);
if (!hasKmipOrgServerCertTable) {
await knex.schema.createTable(TableName.KmipOrgServerCertificates, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("commonName").notNullable();
t.string("altNames").notNullable();
t.string("serialNumber").notNullable();
t.string("keyAlgorithm").notNullable();
t.datetime("issuedAt").notNullable();
t.datetime("expiration").notNullable();
t.binary("encryptedCertificate").notNullable();
t.binary("encryptedChain").notNullable();
});
}
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
if (!hasKmipClientCertTable) {
await knex.schema.createTable(TableName.KmipClientCertificates, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("kmipClientId").notNullable();
t.foreign("kmipClientId").references("id").inTable(TableName.KmipClient).onDelete("CASCADE");
t.string("serialNumber").notNullable();
t.string("keyAlgorithm").notNullable();
t.datetime("issuedAt").notNullable();
t.datetime("expiration").notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig);
if (hasKmipOrgPkiConfig) {
await knex.schema.dropTable(TableName.KmipOrgConfig);
await dropOnUpdateTrigger(knex, TableName.KmipOrgConfig);
}
const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates);
if (hasKmipOrgServerCertTable) {
await knex.schema.dropTable(TableName.KmipOrgServerCertificates);
}
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
if (hasKmipClientCertTable) {
await knex.schema.dropTable(TableName.KmipClientCertificates);
}
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
if (hasKmipClientTable) {
await knex.schema.dropTable(TableName.KmipClient);
}
}

@ -31,7 +31,7 @@ export async function up(knex: Knex): Promise<void> {
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
const projectEncryptionRingBuffer =
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
const webhooks = await knex(TableName.Webhook)
const webhooks = await knex(TableName.Webhook)
.where({})
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`)
.select(
@ -53,10 +53,13 @@ export async function up(knex: Knex): Promise<void> {
webhooks.map(async (el) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(el.projectId);
if (!projectKmsService) {
projectKmsService = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: el.projectId
}, knex);
projectKmsService = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.SecretManager,
projectId: el.projectId
},
knex
);
projectEncryptionRingBuffer.push(el.projectId, projectKmsService);
}

@ -46,10 +46,13 @@ export async function up(knex: Knex): Promise<void> {
dynamicSecretRootCredentials.map(async ({ projectId, ...el }) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(projectId);
if (!projectKmsService) {
projectKmsService = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
}, knex);
projectKmsService = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.SecretManager,
projectId
},
knex
);
projectEncryptionRingBuffer.push(projectId, projectKmsService);
}

@ -39,10 +39,13 @@ export async function up(knex: Knex): Promise<void> {
secretRotations.map(async ({ projectId, ...el }) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(projectId);
if (!projectKmsService) {
projectKmsService = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
}, knex);
projectKmsService = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.SecretManager,
projectId
},
knex
);
projectEncryptionRingBuffer.push(projectId, projectKmsService);
}

@ -76,77 +76,87 @@ const reencryptIdentityK8sAuth = async (knex: Knex) => {
)
.orderBy(`${TableName.OrgBot}.orgId` as "orgId");
const updatedIdentityKubernetesConfigs = [];
const updatedIdentityKubernetesConfigs = [];
for (const { encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, orgId, ...el } of identityKubernetesConfigs) {
let orgKmsService = orgEncryptionRingBuffer.getItem(orgId);
if (!orgKmsService) {
orgKmsService = await kmsService.createCipherPairWithDataKey({
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
});
},
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)

@ -62,10 +62,13 @@ const reencryptIdentityOidcAuth = async (knex: Knex) => {
async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, orgId, ...el }) => {
let orgKmsService = orgEncryptionRingBuffer.getItem(orgId);
if (!orgKmsService) {
orgKmsService = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
}, knex);
orgKmsService = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.Organization,
orgId
},
knex
);
orgEncryptionRingBuffer.push(orgId, orgKmsService);
}
const key = infisicalSymmetricDecrypt({

@ -49,10 +49,13 @@ const reencryptSamlConfig = async (knex: Knex) => {
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);
orgKmsService = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.Organization,
orgId: el.orgId
},
knex
);
orgEncryptionRingBuffer.push(el.orgId, orgKmsService);
}
const key = infisicalSymmetricDecrypt({
@ -204,10 +207,13 @@ const reencryptLdapConfig = async (knex: Knex) => {
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);
orgKmsService = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.Organization,
orgId: el.orgId
},
knex
);
orgEncryptionRingBuffer.push(el.orgId, orgKmsService);
}
const key = infisicalSymmetricDecrypt({
@ -353,10 +359,13 @@ const reencryptOidcConfig = async (knex: Knex) => {
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);
orgKmsService = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.Organization,
orgId: el.orgId
},
knex
);
orgEncryptionRingBuffer.push(el.orgId, orgKmsService);
}
const key = infisicalSymmetricDecrypt({

@ -42,7 +42,7 @@ export const getMigrationEnvConfig = () => {
console.error("Invalid environment variables. Check the error below");
// eslint-disable-next-line no-console
console.error(
"Migration is now automatic at startup. Please remove this step from your workflow and start the application as normal."
"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);

@ -45,6 +45,10 @@ export * from "./incident-contacts";
export * from "./integration-auths";
export * from "./integrations";
export * from "./internal-kms";
export * from "./kmip-client-certificates";
export * from "./kmip-clients";
export * from "./kmip-org-configs";
export * from "./kmip-org-server-certificates";
export * from "./kms-key-versions";
export * from "./kms-keys";
export * from "./kms-root-config";

@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const KmipClientCertificatesSchema = z.object({
id: z.string().uuid(),
kmipClientId: z.string().uuid(),
serialNumber: z.string(),
keyAlgorithm: z.string(),
issuedAt: z.date(),
expiration: z.date()
});
export type TKmipClientCertificates = z.infer<typeof KmipClientCertificatesSchema>;
export type TKmipClientCertificatesInsert = Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>;
export type TKmipClientCertificatesUpdate = Partial<
Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>
>;

@ -0,0 +1,20 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const KmipClientsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
permissions: z.string().array().nullable().optional(),
description: z.string().nullable().optional(),
projectId: z.string()
});
export type TKmipClients = z.infer<typeof KmipClientsSchema>;
export type TKmipClientsInsert = Omit<z.input<typeof KmipClientsSchema>, TImmutableDBKeys>;
export type TKmipClientsUpdate = Partial<Omit<z.input<typeof KmipClientsSchema>, TImmutableDBKeys>>;

@ -0,0 +1,39 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmipOrgConfigsSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
caKeyAlgorithm: z.string(),
rootCaIssuedAt: z.date(),
rootCaExpiration: z.date(),
rootCaSerialNumber: z.string(),
encryptedRootCaCertificate: zodBuffer,
encryptedRootCaPrivateKey: zodBuffer,
serverIntermediateCaIssuedAt: z.date(),
serverIntermediateCaExpiration: z.date(),
serverIntermediateCaSerialNumber: z.string().nullable().optional(),
encryptedServerIntermediateCaCertificate: zodBuffer,
encryptedServerIntermediateCaChain: zodBuffer,
encryptedServerIntermediateCaPrivateKey: zodBuffer,
clientIntermediateCaIssuedAt: z.date(),
clientIntermediateCaExpiration: z.date(),
clientIntermediateCaSerialNumber: z.string(),
encryptedClientIntermediateCaCertificate: zodBuffer,
encryptedClientIntermediateCaChain: zodBuffer,
encryptedClientIntermediateCaPrivateKey: zodBuffer,
createdAt: z.date(),
updatedAt: z.date()
});
export type TKmipOrgConfigs = z.infer<typeof KmipOrgConfigsSchema>;
export type TKmipOrgConfigsInsert = Omit<z.input<typeof KmipOrgConfigsSchema>, TImmutableDBKeys>;
export type TKmipOrgConfigsUpdate = Partial<Omit<z.input<typeof KmipOrgConfigsSchema>, TImmutableDBKeys>>;

@ -0,0 +1,29 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmipOrgServerCertificatesSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
commonName: z.string(),
altNames: z.string(),
serialNumber: z.string(),
keyAlgorithm: z.string(),
issuedAt: z.date(),
expiration: z.date(),
encryptedCertificate: zodBuffer,
encryptedChain: zodBuffer
});
export type TKmipOrgServerCertificates = z.infer<typeof KmipOrgServerCertificatesSchema>;
export type TKmipOrgServerCertificatesInsert = Omit<z.input<typeof KmipOrgServerCertificatesSchema>, TImmutableDBKeys>;
export type TKmipOrgServerCertificatesUpdate = Partial<
Omit<z.input<typeof KmipOrgServerCertificatesSchema>, TImmutableDBKeys>
>;

@ -132,7 +132,11 @@ export enum TableName {
SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs",
AppConnection = "app_connections",
SecretSync = "secret_syncs"
SecretSync = "secret_syncs",
KmipClient = "kmip_clients",
KmipOrgConfig = "kmip_org_configs",
KmipOrgServerCertificates = "kmip_org_server_certificates",
KmipClientCertificates = "kmip_client_certificates"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

@ -9,6 +9,8 @@ import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerKmipRouter } from "./kmip-router";
import { registerKmipSpecRouter } from "./kmip-spec-router";
import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
import { registerOidcRouter } from "./oidc-router";
@ -110,4 +112,12 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
});
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
await server.register(
async (kmipRouter) => {
await kmipRouter.register(registerKmipRouter);
await kmipRouter.register(registerKmipSpecRouter, { prefix: "/spec" });
},
{ prefix: "/kmip" }
);
};

@ -0,0 +1,428 @@
import ms from "ms";
import { z } from "zod";
import { KmipClientsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators";
const KmipClientResponseSchema = KmipClientsSchema.pick({
projectId: true,
name: true,
id: true,
description: true,
permissions: true
});
export const registerKmipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/clients",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
projectId: z.string(),
name: z.string().trim().min(1),
description: z.string().optional(),
permissions: z.nativeEnum(KmipPermission).array()
}),
response: {
200: KmipClientResponseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmipClient = await server.services.kmip.createKmipClient({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: kmipClient.projectId,
event: {
type: EventType.CREATE_KMIP_CLIENT,
metadata: {
id: kmipClient.id,
name: kmipClient.name,
permissions: (kmipClient.permissions ?? []) as KmipPermission[]
}
}
});
return kmipClient;
}
});
server.route({
method: "PATCH",
url: "/clients/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
name: z.string().trim().min(1),
description: z.string().optional(),
permissions: z.nativeEnum(KmipPermission).array()
}),
response: {
200: KmipClientResponseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmipClient = await server.services.kmip.updateKmipClient({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: kmipClient.projectId,
event: {
type: EventType.UPDATE_KMIP_CLIENT,
metadata: {
id: kmipClient.id,
name: kmipClient.name,
permissions: (kmipClient.permissions ?? []) as KmipPermission[]
}
}
});
return kmipClient;
}
});
server.route({
method: "DELETE",
url: "/clients/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: KmipClientResponseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmipClient = await server.services.kmip.deleteKmipClient({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: kmipClient.projectId,
event: {
type: EventType.DELETE_KMIP_CLIENT,
metadata: {
id: kmipClient.id
}
}
});
return kmipClient;
}
});
server.route({
method: "GET",
url: "/clients/:id",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: KmipClientResponseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmipClient = await server.services.kmip.getKmipClient({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: kmipClient.projectId,
event: {
type: EventType.GET_KMIP_CLIENT,
metadata: {
id: kmipClient.id
}
}
});
return kmipClient;
}
});
server.route({
method: "GET",
url: "/clients",
config: {
rateLimit: readLimit
},
schema: {
description: "List KMIP clients",
querystring: z.object({
projectId: z.string(),
offset: z.coerce.number().min(0).optional().default(0),
limit: z.coerce.number().min(1).max(100).optional().default(100),
orderBy: z.nativeEnum(KmipClientOrderBy).optional().default(KmipClientOrderBy.Name),
orderDirection: z.nativeEnum(OrderByDirection).optional().default(OrderByDirection.ASC),
search: z.string().trim().optional()
}),
response: {
200: z.object({
kmipClients: KmipClientResponseSchema.array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.GET_KMIP_CLIENTS,
metadata: {
ids: kmipClients.map((key) => key.id)
}
}
});
return { kmipClients, totalCount };
}
});
server.route({
method: "POST",
url: "/clients/:id/certificates",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
}),
response: {
200: z.object({
serialNumber: z.string(),
certificateChain: z.string(),
certificate: z.string(),
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificate = await server.services.kmip.createKmipClientCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
clientId: req.params.id,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: certificate.projectId,
event: {
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE,
metadata: {
clientId: req.params.id,
serialNumber: certificate.serialNumber,
ttl: req.body.ttl,
keyAlgorithm: req.body.keyAlgorithm
}
}
});
return certificate;
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
}),
response: {
200: z.object({
serverCertificateChain: z.string(),
clientCertificateChain: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const chains = await server.services.kmip.setupOrgKmip({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.SETUP_KMIP,
metadata: {
keyAlgorithm: req.body.caKeyAlgorithm
}
}
});
return chains;
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
serverCertificateChain: z.string(),
clientCertificateChain: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmip = await server.services.kmip.getOrgKmip({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_KMIP,
metadata: {
id: kmip.id
}
}
});
return kmip;
}
});
server.route({
method: "POST",
url: "/server-registration",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
hostnamesOrIps: validateAltNamesField,
commonName: z.string().trim().min(1).optional(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional().default(CertKeyAlgorithm.RSA_2048),
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
}),
response: {
200: z.object({
clientCertificateChain: z.string(),
certificateChain: z.string(),
certificate: z.string(),
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const configs = await server.services.kmip.registerServer({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.REGISTER_KMIP_SERVER,
metadata: {
serverCertificateSerialNumber: configs.serverCertificateSerialNumber,
hostnamesOrIps: req.body.hostnamesOrIps,
commonName: req.body.commonName ?? "kmip-server",
keyAlgorithm: req.body.keyAlgorithm,
ttl: req.body.ttl
}
}
});
return configs;
}
});
};

@ -0,0 +1,477 @@
import z from "zod";
import { KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
server.decorateRequest("kmipUser", null);
server.addHook("onRequest", async (req) => {
const clientId = req.headers["x-kmip-client-id"] as string;
const projectId = req.headers["x-kmip-project-id"] as string;
const clientCertSerialNumber = req.headers["x-kmip-client-certificate-serial-number"] as string;
const serverCertSerialNumber = req.headers["x-kmip-server-certificate-serial-number"] as string;
if (!serverCertSerialNumber) {
throw new ForbiddenRequestError({
message: "Missing server certificate serial number from request"
});
}
if (!clientCertSerialNumber) {
throw new ForbiddenRequestError({
message: "Missing client certificate serial number from request"
});
}
if (!clientId) {
throw new ForbiddenRequestError({
message: "Missing client ID from request"
});
}
if (!projectId) {
throw new ForbiddenRequestError({
message: "Missing project ID from request"
});
}
// TODO: assert that server certificate used is not revoked
// TODO: assert that client certificate used is not revoked
const kmipClient = await server.store.kmipClient.findByProjectAndClientId(projectId, clientId);
if (!kmipClient) {
throw new NotFoundError({
message: "KMIP client cannot be found."
});
}
if (kmipClient.orgId !== req.permission.orgId) {
throw new ForbiddenRequestError({
message: "Client specified in the request does not belong in the organization"
});
}
req.kmipUser = {
projectId,
clientId,
name: kmipClient.name
};
});
server.route({
method: "POST",
url: "/create",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for creating managed objects",
body: z.object({
algorithm: z.nativeEnum(SymmetricEncryption)
}),
response: {
200: KmsKeysSchema
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.create({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
algorithm: req.body.algorithm
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_CREATE,
metadata: {
id: object.id,
algorithm: req.body.algorithm
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/get",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for getting managed objects",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
value: z.string(),
algorithm: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.get({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_GET,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/get-attributes",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for getting attributes of managed object",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
algorithm: z.string(),
isActive: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.getAttributes({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_GET_ATTRIBUTES,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/destroy",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for destroying managed objects",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.destroy({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_DESTROY,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/activate",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for activating managed object",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
isActive: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.activate({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_ACTIVATE,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/revoke",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for revoking managed object",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
updatedAt: z.date()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.revoke({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_REVOKE,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/locate",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for locating managed objects",
response: {
200: z.object({
objects: z
.object({
id: z.string(),
name: z.string(),
isActive: z.boolean(),
algorithm: z.string(),
createdAt: z.date(),
updatedAt: z.date()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const objects = await server.services.kmipOperation.locate({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_LOCATE,
metadata: {
ids: objects.map((obj) => obj.id)
}
}
});
return {
objects
};
}
});
server.route({
method: "POST",
url: "/register",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for registering managed object",
body: z.object({
key: z.string(),
name: z.string(),
algorithm: z.nativeEnum(SymmetricEncryption)
}),
response: {
200: z.object({
id: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.register({
...req.kmipUser,
...req.body,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_REGISTER,
metadata: {
id: object.id,
algorithm: req.body.algorithm,
name: object.name
}
}
});
return object;
}
});
};

@ -21,6 +21,8 @@ import {
TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types";
import { KmipPermission } from "../kmip/kmip-enum";
export type TListProjectAuditLogDTO = {
filter: {
userAgentType?: UserAgentType;
@ -39,7 +41,14 @@ export type TListProjectAuditLogDTO = {
export type TCreateAuditLogDTO = {
event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor;
actor:
| UserActor
| IdentityActor
| ServiceActor
| ScimClientActor
| PlatformActor
| UnknownUserActor
| KmipClientActor;
orgId?: string;
projectId?: string;
} & BaseAuthData;
@ -252,7 +261,26 @@ export enum EventType {
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
CREATE_KMIP_CLIENT = "create-kmip-client",
UPDATE_KMIP_CLIENT = "update-kmip-client",
DELETE_KMIP_CLIENT = "delete-kmip-client",
GET_KMIP_CLIENT = "get-kmip-client",
GET_KMIP_CLIENTS = "get-kmip-clients",
CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate",
SETUP_KMIP = "setup-kmip",
GET_KMIP = "get-kmip",
REGISTER_KMIP_SERVER = "register-kmip-server",
KMIP_OPERATION_CREATE = "kmip-operation-create",
KMIP_OPERATION_GET = "kmip-operation-get",
KMIP_OPERATION_DESTROY = "kmip-operation-destroy",
KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes",
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
KMIP_OPERATION_REGISTER = "kmip-operation-register"
}
interface UserActorMetadata {
@ -275,6 +303,11 @@ interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
interface KmipClientActorMetadata {
clientId: string;
name: string;
}
interface UnknownUserActorMetadata {}
export interface UserActor {
@ -292,6 +325,11 @@ export interface PlatformActor {
metadata: PlatformActorMetadata;
}
export interface KmipClientActor {
type: ActorType.KMIP_CLIENT;
metadata: KmipClientActorMetadata;
}
export interface UnknownUserActor {
type: ActorType.UNKNOWN_USER;
metadata: UnknownUserActorMetadata;
@ -307,7 +345,7 @@ export interface ScimClientActor {
metadata: ScimClientActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor | KmipClientActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
@ -352,6 +390,7 @@ interface CreateSecretBatchEvent {
secrets: Array<{
secretId: string;
secretKey: string;
secretPath?: string;
secretVersion: number;
secretMetadata?: TSecretMetadata;
}>;
@ -374,8 +413,14 @@ interface UpdateSecretBatchEvent {
type: EventType.UPDATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number; secretMetadata?: TSecretMetadata }>;
secretPath?: string;
secrets: Array<{
secretId: string;
secretKey: string;
secretVersion: number;
secretMetadata?: TSecretMetadata;
secretPath?: string;
}>;
};
}
@ -2084,6 +2129,139 @@ interface OidcGroupMembershipMappingRemoveUserEvent {
};
}
interface CreateKmipClientEvent {
type: EventType.CREATE_KMIP_CLIENT;
metadata: {
name: string;
id: string;
permissions: KmipPermission[];
};
}
interface UpdateKmipClientEvent {
type: EventType.UPDATE_KMIP_CLIENT;
metadata: {
name: string;
id: string;
permissions: KmipPermission[];
};
}
interface DeleteKmipClientEvent {
type: EventType.DELETE_KMIP_CLIENT;
metadata: {
id: string;
};
}
interface GetKmipClientEvent {
type: EventType.GET_KMIP_CLIENT;
metadata: {
id: string;
};
}
interface GetKmipClientsEvent {
type: EventType.GET_KMIP_CLIENTS;
metadata: {
ids: string[];
};
}
interface CreateKmipClientCertificateEvent {
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE;
metadata: {
clientId: string;
ttl: string;
keyAlgorithm: string;
serialNumber: string;
};
}
interface KmipOperationGetEvent {
type: EventType.KMIP_OPERATION_GET;
metadata: {
id: string;
};
}
interface KmipOperationDestroyEvent {
type: EventType.KMIP_OPERATION_DESTROY;
metadata: {
id: string;
};
}
interface KmipOperationCreateEvent {
type: EventType.KMIP_OPERATION_CREATE;
metadata: {
id: string;
algorithm: string;
};
}
interface KmipOperationGetAttributesEvent {
type: EventType.KMIP_OPERATION_GET_ATTRIBUTES;
metadata: {
id: string;
};
}
interface KmipOperationActivateEvent {
type: EventType.KMIP_OPERATION_ACTIVATE;
metadata: {
id: string;
};
}
interface KmipOperationRevokeEvent {
type: EventType.KMIP_OPERATION_REVOKE;
metadata: {
id: string;
};
}
interface KmipOperationLocateEvent {
type: EventType.KMIP_OPERATION_LOCATE;
metadata: {
ids: string[];
};
}
interface KmipOperationRegisterEvent {
type: EventType.KMIP_OPERATION_REGISTER;
metadata: {
id: string;
algorithm: string;
name: string;
};
}
interface SetupKmipEvent {
type: EventType.SETUP_KMIP;
metadata: {
keyAlgorithm: CertKeyAlgorithm;
};
}
interface GetKmipEvent {
type: EventType.GET_KMIP;
metadata: {
id: string;
};
}
interface RegisterKmipServerEvent {
type: EventType.REGISTER_KMIP_SERVER;
metadata: {
serverCertificateSerialNumber: string;
hostnamesOrIps: string;
commonName: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -2275,4 +2453,21 @@ export type Event =
| SecretSyncImportSecretsEvent
| SecretSyncRemoveSecretsEvent
| OidcGroupMembershipMappingAssignUserEvent
| OidcGroupMembershipMappingRemoveUserEvent;
| OidcGroupMembershipMappingRemoveUserEvent
| CreateKmipClientEvent
| UpdateKmipClientEvent
| DeleteKmipClientEvent
| GetKmipClientEvent
| GetKmipClientsEvent
| CreateKmipClientCertificateEvent
| SetupKmipEvent
| GetKmipEvent
| RegisterKmipServerEvent
| KmipOperationGetEvent
| KmipOperationDestroyEvent
| KmipOperationCreateEvent
| KmipOperationGetAttributesEvent
| KmipOperationActivateEvent
| KmipOperationRevokeEvent
| KmipOperationLocateEvent
| KmipOperationRegisterEvent;

@ -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}%`);
}

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipClientCertificateDALFactory = ReturnType<typeof kmipClientCertificateDALFactory>;
export const kmipClientCertificateDALFactory = (db: TDbClient) => {
const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates);
return kmipClientCertOrm;
};

@ -0,0 +1,86 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TKmipClients } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { KmipClientOrderBy } from "./kmip-types";
export type TKmipClientDALFactory = ReturnType<typeof kmipClientDALFactory>;
export const kmipClientDALFactory = (db: TDbClient) => {
const kmipClientOrm = ormify(db, TableName.KmipClient);
const findByProjectAndClientId = async (projectId: string, clientId: string) => {
try {
const client = await db
.replicaNode()(TableName.KmipClient)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.KmipClient}.projectId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
.where({
[`${TableName.KmipClient}.projectId` as "projectId"]: projectId,
[`${TableName.KmipClient}.id` as "id"]: clientId
})
.select(selectAllTableCols(TableName.KmipClient))
.select(db.ref("id").withSchema(TableName.Organization).as("orgId"))
.first();
return client;
} catch (error) {
throw new DatabaseError({ error, name: "Find by project and client ID" });
}
};
const findByProjectId = async (
{
projectId,
offset = 0,
limit,
orderBy = KmipClientOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search
}: {
projectId: string;
offset?: number;
limit?: number;
orderBy?: KmipClientOrderBy;
orderDirection?: OrderByDirection;
search?: string;
},
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.KmipClient)
.where("projectId", projectId)
.where((qb) => {
if (search) {
void qb.whereILike("name", `%${search}%`);
}
})
.select<
(TKmipClients & {
total_count: number;
})[]
>(selectAllTableCols(TableName.KmipClient), db.raw(`count(*) OVER() as total_count`))
.orderBy(orderBy, orderDirection);
if (limit) {
void query.limit(limit).offset(offset);
}
const data = await query;
return { kmipClients: data, totalCount: Number(data?.[0]?.total_count ?? 0) };
} catch (error) {
throw new DatabaseError({ error, name: "Find KMIP clients by project id" });
}
};
return {
...kmipClientOrm,
findByProjectId,
findByProjectAndClientId
};
};

@ -0,0 +1,11 @@
export enum KmipPermission {
Create = "create",
Locate = "locate",
Check = "check",
Get = "get",
GetAttributes = "get-attributes",
Activate = "activate",
Revoke = "revoke",
Destroy = "destroy",
Register = "register"
}

@ -0,0 +1,422 @@
import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TKmipClientDALFactory } from "./kmip-client-dal";
import { KmipPermission } from "./kmip-enum";
import {
TKmipCreateDTO,
TKmipDestroyDTO,
TKmipGetAttributesDTO,
TKmipGetDTO,
TKmipLocateDTO,
TKmipRegisterDTO,
TKmipRevokeDTO
} from "./kmip-types";
type TKmipOperationServiceFactoryDep = {
kmsService: TKmsServiceFactory;
kmsDAL: TKmsKeyDALFactory;
kmipClientDAL: TKmipClientDALFactory;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId" | "findById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
export type TKmipOperationServiceFactory = ReturnType<typeof kmipOperationServiceFactory>;
export const kmipOperationServiceFactory = ({
kmsService,
kmsDAL,
projectDAL,
kmipClientDAL,
permissionService
}: TKmipOperationServiceFactoryDep) => {
const create = async ({
projectId,
clientId,
algorithm,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TKmipCreateDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Create)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP create"
});
}
const kmsKey = await kmsService.generateKmsKey({
encryptionAlgorithm: algorithm,
orgId: actorOrgId,
projectId,
isReserved: false
});
return kmsKey;
};
const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Destroy)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP destroy"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
if (key.isReserved) {
throw new BadRequestError({ message: "Cannot destroy reserved keys" });
}
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
if (!completeKeyDetails.internalKms) {
throw new BadRequestError({
message: "Cannot destroy external keys"
});
}
if (!completeKeyDetails.isDisabled) {
throw new BadRequestError({
message: "Cannot destroy active keys"
});
}
const kms = kmsDAL.deleteById(id);
return kms;
};
const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Get)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP get"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
if (key.isReserved) {
throw new BadRequestError({ message: "Cannot get reserved keys" });
}
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
if (!completeKeyDetails.internalKms) {
throw new BadRequestError({
message: "Cannot get external keys"
});
}
const kmsKey = await kmsService.getKeyMaterial({
kmsId: key.id
});
return {
id: key.id,
value: kmsKey.toString("base64"),
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
isActive: !key.isDisabled,
createdAt: key.createdAt,
updatedAt: key.updatedAt
};
};
const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Activate)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP activate"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
return {
id: key.id,
isActive: !key.isDisabled
};
};
const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Revoke)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP revoke"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
if (key.isReserved) {
throw new BadRequestError({ message: "Cannot revoke reserved keys" });
}
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
if (!completeKeyDetails.internalKms) {
throw new BadRequestError({
message: "Cannot revoke external keys"
});
}
const revokedKey = await kmsDAL.updateById(key.id, {
isDisabled: true
});
return {
id: key.id,
updatedAt: revokedKey.updatedAt
};
};
const getAttributes = async ({
projectId,
id,
clientId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TKmipGetAttributesDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.GetAttributes)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP get attributes"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
if (key.isReserved) {
throw new BadRequestError({ message: "Cannot get reserved keys" });
}
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
if (!completeKeyDetails.internalKms) {
throw new BadRequestError({
message: "Cannot get external keys"
});
}
return {
id: key.id,
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
isActive: !key.isDisabled,
createdAt: key.createdAt,
updatedAt: key.updatedAt
};
};
const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Locate)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP locate"
});
}
const keys = await kmsDAL.findProjectCmeks(projectId);
return keys;
};
const register = async ({
projectId,
clientId,
key,
algorithm,
name,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TKmipRegisterDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Register)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP register"
});
}
const project = await projectDAL.findById(projectId);
const kmsKey = await kmsService.importKeyMaterial({
name,
key: Buffer.from(key, "base64"),
algorithm,
isReserved: false,
projectId,
orgId: project.orgId
});
return kmsKey;
};
return {
create,
get,
activate,
getAttributes,
destroy,
revoke,
locate,
register
};
};

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipOrgConfigDALFactory = ReturnType<typeof kmipOrgConfigDALFactory>;
export const kmipOrgConfigDALFactory = (db: TDbClient) => {
const kmipOrgConfigOrm = ormify(db, TableName.KmipOrgConfig);
return kmipOrgConfigOrm;
};

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipOrgServerCertificateDALFactory = ReturnType<typeof kmipOrgServerCertificateDALFactory>;
export const kmipOrgServerCertificateDALFactory = (db: TDbClient) => {
const kmipOrgServerCertificateOrm = ormify(db, TableName.KmipOrgServerCertificates);
return kmipOrgServerCertificateOrm;
};

@ -0,0 +1,817 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidHostname, isValidIp } from "@app/lib/ip";
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import {
createSerialNumber,
keyAlgorithmToAlgCfg
} from "@app/services/certificate-authority/certificate-authority-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission";
import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal";
import { TKmipClientDALFactory } from "./kmip-client-dal";
import { TKmipOrgConfigDALFactory } from "./kmip-org-config-dal";
import { TKmipOrgServerCertificateDALFactory } from "./kmip-org-server-certificate-dal";
import {
TCreateKmipClientCertificateDTO,
TCreateKmipClientDTO,
TDeleteKmipClientDTO,
TGenerateOrgKmipServerCertificateDTO,
TGetKmipClientDTO,
TGetOrgKmipDTO,
TListKmipClientsByProjectIdDTO,
TRegisterServerDTO,
TSetupOrgKmipDTO,
TUpdateKmipClientDTO
} from "./kmip-types";
type TKmipServiceFactoryDep = {
kmipClientDAL: TKmipClientDALFactory;
kmipClientCertificateDAL: TKmipClientCertificateDALFactory;
kmipOrgServerCertificateDAL: TKmipOrgServerCertificateDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
kmipOrgConfigDAL: TKmipOrgConfigDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>;
export const kmipServiceFactory = ({
kmipClientDAL,
permissionService,
kmipClientCertificateDAL,
kmipOrgConfigDAL,
kmsService,
kmipOrgServerCertificateDAL,
licenseService
}: TKmipServiceFactoryDep) => {
const createKmipClient = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
name,
description,
permissions
}: TCreateKmipClientDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionSub.Kmip
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to create KMIP client. Upgrade your plan to enterprise."
});
const kmipClient = await kmipClientDAL.create({
projectId,
name,
description,
permissions
});
return kmipClient;
};
const updateKmipClient = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
name,
description,
permissions,
id
}: TUpdateKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to update KMIP client. Upgrade your plan to enterprise."
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionSub.Kmip
);
const updatedKmipClient = await kmipClientDAL.updateById(id, {
name,
description,
permissions
});
return updatedKmipClient;
};
const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionSub.Kmip
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to delete KMIP client. Upgrade your plan to enterprise."
});
const deletedKmipClient = await kmipClientDAL.deleteById(id);
return deletedKmipClient;
};
const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
return kmipClient;
};
const listKmipClientsByProjectId = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
...rest
}: TListKmipClientsByProjectIdDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
return kmipClientDAL.findByProjectId({ projectId, ...rest });
};
const createKmipClientCertificate = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
ttl,
keyAlgorithm,
clientId
}: TCreateKmipClientCertificateDTO) => {
const kmipClient = await kmipClientDAL.findById(clientId);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${clientId} does not exist`
});
}
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to create KMIP client. Upgrade your plan to enterprise."
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.GenerateClientCertificates,
ProjectPermissionSub.Kmip
);
const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId
});
if (!kmipConfig) {
throw new InternalServerError({
message: "KMIP has not been configured for the organization"
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const caCertObj = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate })
);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
];
const caAlg = keyAlgorithmToAlgCfg(kmipConfig.caKeyAlgorithm as CertKeyAlgorithm);
const caSkObj = crypto.createPrivateKey({
key: decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaPrivateKey }),
format: "der",
type: "pkcs8"
});
const caPrivateKey = await crypto.subtle.importKey(
"pkcs8",
caSkObj.export({ format: "der", type: "pkcs8" }),
caAlg,
true,
["sign"]
);
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: `OU=${kmipClient.projectId},CN=${clientId}`,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: leafKeys.publicKey,
signingAlgorithm: alg,
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate }));
const serverIntermediateCaCert = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate })
);
await kmipClientCertificateDAL.create({
kmipClientId: clientId,
keyAlgorithm,
issuedAt: notBeforeDate,
expiration: notAfterDate,
serialNumber
});
return {
serialNumber,
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
certificate: leafCert.toString("pem"),
certificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
projectId: kmipClient.projectId
};
};
const getServerCertificateBySerialNumber = async (orgId: string, serialNumber: string) => {
const serverCert = await kmipOrgServerCertificateDAL.findOne({
serialNumber,
orgId
});
if (!serverCert) {
throw new NotFoundError({
message: "Server certificate not found"
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const parsedCertificate = new x509.X509Certificate(decryptor({ cipherTextBlob: serverCert.encryptedCertificate }));
return {
publicKey: parsedCertificate.publicKey.toString("pem"),
keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm
};
};
const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId
});
if (kmipConfig) {
throw new BadRequestError({
message: "KMIP has already been configured for the organization"
});
}
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to setup KMIP. Upgrade your plan to enterprise."
});
const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm);
// generate root CA
const rootCaSerialNumber = createSerialNumber();
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey);
const rootCaIssuedAt = new Date();
const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20));
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
name: `CN=KMIP Root CA,OU=${actorOrgId}`,
serialNumber: rootCaSerialNumber,
notBefore: rootCaIssuedAt,
notAfter: rootCaExpiration,
signingAlgorithm: alg,
keys: rootCaKeys,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
]
});
// generate intermediate server CA
const serverIntermediateCaSerialNumber = createSerialNumber();
const serverIntermediateCaIssuedAt = new Date();
const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey);
const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({
serialNumber: serverIntermediateCaSerialNumber,
subject: `CN=KMIP Server Intermediate CA,OU=${actorOrgId}`,
issuer: rootCaCert.subject,
notBefore: serverIntermediateCaIssuedAt,
notAfter: serverIntermediateCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: serverIntermediateCaKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(true, 0, true),
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey)
]
});
// generate intermediate client CA
const clientIntermediateCaSerialNumber = createSerialNumber();
const clientIntermediateCaIssuedAt = new Date();
const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey);
const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({
serialNumber: clientIntermediateCaSerialNumber,
subject: `CN=KMIP Client Intermediate CA,OU=${actorOrgId}`,
issuer: rootCaCert.subject,
notBefore: clientIntermediateCaIssuedAt,
notAfter: clientIntermediateCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: clientIntermediateCaKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(true, 0, true),
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey)
]
});
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
await kmipOrgConfigDAL.create({
orgId: actorOrgId,
caKeyAlgorithm,
rootCaIssuedAt,
rootCaExpiration,
rootCaSerialNumber,
encryptedRootCaCertificate: encryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
encryptedRootCaPrivateKey: encryptor({
plainText: rootCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob,
serverIntermediateCaIssuedAt,
serverIntermediateCaExpiration,
serverIntermediateCaSerialNumber,
encryptedServerIntermediateCaCertificate: encryptor({
plainText: Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData))
}).cipherTextBlob,
encryptedServerIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) })
.cipherTextBlob,
encryptedServerIntermediateCaPrivateKey: encryptor({
plainText: serverIntermediateCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob,
clientIntermediateCaIssuedAt,
clientIntermediateCaExpiration,
clientIntermediateCaSerialNumber,
encryptedClientIntermediateCaCertificate: encryptor({
plainText: Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData))
}).cipherTextBlob,
encryptedClientIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) })
.cipherTextBlob,
encryptedClientIntermediateCaPrivateKey: encryptor({
plainText: clientIntermediateCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob
});
return {
serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert])
};
};
const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => {
await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId);
const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId
});
if (!kmipConfig) {
throw new BadRequestError({
message: "KMIP has not been configured for the organization"
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate }));
const serverIntermediateCaCert = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate })
);
const clientIntermediateCaCert = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate })
);
return {
id: kmipConfig.id,
serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert])
};
};
const generateOrgKmipServerCertificate = async ({
orgId,
ttl,
commonName,
altNames,
keyAlgorithm
}: TGenerateOrgKmipServerCertificateDTO) => {
const kmipOrgConfig = await kmipOrgConfigDAL.findOne({
orgId
});
if (!kmipOrgConfig) {
throw new BadRequestError({
message: "KMIP has not been configured for the organization"
});
}
const plan = await licenseService.getPlan(orgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to generate KMIP server certificate. Upgrade your plan to enterprise."
});
const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const caCertObj = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaCertificate })
);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true)
];
const altNamesArray: {
type: "email" | "dns" | "ip";
value: string;
}[] = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
if (isValidHostname(altName)) {
return {
type: "dns",
value: altName
};
}
if (isValidIp(altName)) {
return {
type: "ip",
value: altName
};
}
throw new Error(`Invalid altName: ${altName}`);
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
const caAlg = keyAlgorithmToAlgCfg(kmipOrgConfig.caKeyAlgorithm as CertKeyAlgorithm);
const decryptedCaCertChain = decryptor({
cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaChain
}).toString("utf-8");
const caSkObj = crypto.createPrivateKey({
key: decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaPrivateKey }),
format: "der",
type: "pkcs8"
});
const caPrivateKey = await crypto.subtle.importKey(
"pkcs8",
caSkObj.export({ format: "der", type: "pkcs8" }),
caAlg,
true,
["sign"]
);
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: `CN=${commonName}`,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: leafKeys.publicKey,
signingAlgorithm: alg,
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim();
await kmipOrgServerCertificateDAL.create({
orgId,
keyAlgorithm,
issuedAt: notBeforeDate,
expiration: notAfterDate,
serialNumber,
commonName,
altNames,
encryptedCertificate: encryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }).cipherTextBlob,
encryptedChain: encryptor({ plainText: Buffer.from(certificateChain) }).cipherTextBlob
});
return {
serialNumber,
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
certificate: leafCert.toString("pem"),
certificateChain
};
};
const registerServer = async ({
actorOrgId,
actor,
actorId,
actorAuthMethod,
ttl,
commonName,
keyAlgorithm,
hostnamesOrIps
}: TRegisterServerDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId
});
if (!kmipConfig) {
throw new BadRequestError({
message: "KMIP has not been configured for the organization"
});
}
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to register KMIP server. Upgrade your plan to enterprise."
});
const { privateKey, certificate, certificateChain, serialNumber } = await generateOrgKmipServerCertificate({
orgId: actorOrgId,
commonName: commonName ?? "kmip-server",
altNames: hostnamesOrIps,
keyAlgorithm: keyAlgorithm ?? (kmipConfig.caKeyAlgorithm as CertKeyAlgorithm),
ttl
});
const { clientCertificateChain } = await getOrgKmip({
actor,
actorAuthMethod,
actorId,
actorOrgId
});
return {
serverCertificateSerialNumber: serialNumber,
clientCertificateChain,
privateKey,
certificate,
certificateChain
};
};
return {
createKmipClient,
updateKmipClient,
deleteKmipClient,
getKmipClient,
listKmipClientsByProjectId,
createKmipClientCertificate,
setupOrgKmip,
generateOrgKmipServerCertificate,
getOrgKmip,
getServerCertificateBySerialNumber,
registerServer
};
};

@ -0,0 +1,102 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { KmipPermission } from "./kmip-enum";
export type TCreateKmipClientCertificateDTO = {
clientId: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateKmipClientDTO = {
name: string;
description?: string;
permissions: KmipPermission[];
} & TProjectPermission;
export type TUpdateKmipClientDTO = {
id: string;
name?: string;
description?: string;
permissions?: KmipPermission[];
} & Omit<TProjectPermission, "projectId">;
export type TDeleteKmipClientDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetKmipClientDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export enum KmipClientOrderBy {
Name = "name"
}
export type TListKmipClientsByProjectIdDTO = {
offset?: number;
limit?: number;
orderBy?: KmipClientOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & TProjectPermission;
type KmipOperationBaseDTO = {
clientId: string;
projectId: string;
} & Omit<TOrgPermission, "orgId">;
export type TKmipCreateDTO = {
algorithm: SymmetricEncryption;
} & KmipOperationBaseDTO;
export type TKmipGetDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipGetAttributesDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipDestroyDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipActivateDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipRevokeDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipLocateDTO = KmipOperationBaseDTO;
export type TKmipRegisterDTO = {
name: string;
key: string;
algorithm: SymmetricEncryption;
} & KmipOperationBaseDTO;
export type TSetupOrgKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm;
} & Omit<TOrgPermission, "orgId">;
export type TGetOrgKmipDTO = Omit<TOrgPermission, "orgId">;
export type TGenerateOrgKmipServerCertificateDTO = {
commonName: string;
altNames: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
orgId: string;
};
export type TRegisterServerDTO = {
hostnamesOrIps: string;
commonName?: string;
keyAlgorithm?: CertKeyAlgorithm;
ttl: string;
} & Omit<TOrgPermission, "orgId">;

@ -50,7 +50,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
},
pkiEst: false,
enforceMfa: false,
projectTemplates: false
projectTemplates: false,
kmip: false
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

@ -68,6 +68,7 @@ export type TFeatureSet = {
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: false;
kmip: false;
};
export type TOrgPlansTableDTO = {

@ -23,6 +23,11 @@ export enum OrgPermissionAppConnectionActions {
Connect = "connect"
}
export enum OrgPermissionKmipActions {
Proxy = "proxy",
Setup = "setup"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
@ -44,7 +49,8 @@ export enum OrgPermissionSubjects {
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections"
AppConnections = "app-connections",
Kmip = "kmip"
}
export type AppConnectionSubjectFields = {
@ -74,7 +80,8 @@ export type OrgPermissionSet =
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
const AppConnectionConditionSchema = z
.object({
@ -167,6 +174,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
"Describe what action an entity can take."
)
})
]);
@ -253,6 +266,11 @@ const buildAdminPermission = () => {
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
return rules;
};

@ -44,6 +44,14 @@ export enum ProjectPermissionSecretSyncActions {
RemoveSecrets = "remove-secrets"
}
export enum ProjectPermissionKmipActions {
CreateClients = "create-clients",
UpdateClients = "update-clients",
DeleteClients = "delete-clients",
ReadClients = "read-clients",
GenerateClientCertificates = "generate-client-certificates"
}
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
@ -75,7 +83,8 @@ export enum ProjectPermissionSub {
PkiCollections = "pki-collections",
Kms = "kms",
Cmek = "cmek",
SecretSyncs = "secret-syncs"
SecretSyncs = "secret-syncs",
Kmip = "kmip"
}
export type SecretSubjectFields = {
@ -156,6 +165,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
@ -410,6 +420,12 @@ const GeneralPermissionSchema = [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
"Describe what action an entity can take."
)
})
];
@ -575,6 +591,18 @@ const buildAdminPermissionRules = () => {
],
ProjectPermissionSub.SecretSyncs
);
can(
[
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionKmipActions.ReadClients,
ProjectPermissionKmipActions.GenerateClientCertificates
],
ProjectPermissionSub.Kmip
);
return rules;
};

@ -721,7 +721,8 @@ export const RAW_SECRETS = {
secretName: "The name of the secret to update.",
secretComment: "Update comment to the secret.",
environment: "The slug of the environment where the secret is located.",
secretPath: "The path of the secret to update.",
mode: "Defines how the system should handle missing secrets during an update.",
secretPath: "The default path for secrets to update or upsert, if not provided in the secret details.",
secretValue: "The new value of the secret.",
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to update.",
@ -1718,36 +1719,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.",
MAPPING_BEHAVIOR:
"How secrets from Infisical should be mapped to AWS Secrets Manager; one-to-one or many-to-one.",
SECRET_NAME: "The secret name in AWS Secrets Manager to sync to when using mapping behavior many-to-one."
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: {
VAULT_BASE_URL:
"The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/"
vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/"
},
AZURE_APP_CONFIGURATION: {
CONFIGURATION_URL:
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."
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."
}
}
};

@ -103,6 +103,16 @@ export const isValidIpOrCidr = (ip: string): boolean => {
return false;
};
export const isValidIp = (ip: string) => {
return net.isIPv4(ip) || net.isIPv6(ip);
};
export const isValidHostname = (name: string) => {
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
return hostnameRegex.test(name);
};
export type TIp = {
ipAddress: string;
type: IPType;

@ -35,6 +35,12 @@ import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal";
import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
import { kmipOrgConfigDALFactory } from "@app/ee/services/kmip/kmip-org-config-dal";
import { kmipOrgServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-org-server-certificate-dal";
import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
@ -382,6 +388,10 @@ export const registerRoutes = async (
const projectTemplateDAL = projectTemplateDALFactory(db);
const resourceMetadataDAL = resourceMetadataDALFactory(db);
const kmipClientDAL = kmipClientDALFactory(db);
const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db);
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@ -1429,6 +1439,24 @@ export const registerRoutes = async (
keyStore
});
const kmipService = kmipServiceFactory({
kmipClientDAL,
permissionService,
kmipClientCertificateDAL,
kmipOrgConfigDAL,
kmsService,
kmipOrgServerCertificateDAL,
licenseService
});
const kmipOperationService = kmipOperationServiceFactory({
kmsService,
kmsDAL,
projectDAL,
kmipClientDAL,
permissionService
});
await superAdminService.initServerCfg();
// setup the communication with license key server
@ -1527,7 +1555,9 @@ export const registerRoutes = async (
projectTemplate: projectTemplateService,
totp: totpService,
appConnection: appConnectionService,
secretSync: secretSyncService
secretSync: secretSyncService,
kmip: kmipService,
kmipOperation: kmipOperationService
});
const cronJobs: CronJob[] = [];
@ -1539,7 +1569,8 @@ export const registerRoutes = async (
}
server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL
user: userDAL,
kmipClient: kmipClientDAL
});
await server.register(injectIdentity, { userDAL, serviceTokenDAL });

@ -12,6 +12,10 @@ import {
AzureKeyVaultConnectionListItemSchema,
SanitizedAzureKeyVaultConnectionSchema
} from "@app/services/app-connection/azure-key-vault";
import {
DatabricksConnectionListItemSchema,
SanitizedDatabricksConnectionSchema
} from "@app/services/app-connection/databricks";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import { AuthMode } from "@app/services/auth/auth-type";
@ -22,7 +26,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedGitHubConnectionSchema.options,
...SanitizedGcpConnectionSchema.options,
...SanitizedAzureKeyVaultConnectionSchema.options,
...SanitizedAzureAppConfigurationConnectionSchema.options
...SanitizedAzureAppConfigurationConnectionSchema.options,
...SanitizedDatabricksConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@ -30,7 +35,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
GitHubConnectionListItemSchema,
GcpConnectionListItemSchema,
AzureKeyVaultConnectionListItemSchema,
AzureAppConfigurationConnectionListItemSchema
AzureAppConfigurationConnectionListItemSchema,
DatabricksConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

@ -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 };
}
});
};

@ -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;

@ -3,6 +3,7 @@ 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";
@ -14,5 +15,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.GitHub]: registerGitHubConnectionRouter,
[AppConnection.GCP]: registerGcpConnectionRouter,
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
[AppConnection.Databricks]: registerDatabricksConnectionRouter
};

@ -2,10 +2,9 @@ import jwt from "jsonwebtoken";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
export const registerAuthRoutes = async (server: FastifyZodProvider) => {
server.route({
@ -21,18 +20,19 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req, res) => {
const { decodedToken } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
if (req.auth.authMode === AuthMode.JWT) {
await server.services.login.logout(req.permission.id, req.auth.tokenVersionId);
}
await server.services.login.logout(decodedToken.userId, decodedToken.tokenVersionId);
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully logged out" };
}
});
@ -69,37 +69,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const refreshToken = req.cookies.jid;
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
if (!refreshToken)
throw new NotFoundError({
name: "AuthTokenNotFound",
message: "Failed to find refresh token"
});
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
throw new UnauthorizedError({
message: "The token provided is not a refresh token",
name: "InvalidToken"
});
const tokenVersion = await server.services.authToken.getUserTokenSessionById(
decodedToken.tokenVersionId,
decodedToken.userId
);
if (!tokenVersion)
throw new UnauthorizedError({
message: "Valid token version not found",
name: "InvalidToken"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
throw new UnauthorizedError({
message: "Token version mismatch",
name: "InvalidToken"
});
}
const token = jwt.sign(
{

@ -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
});

@ -4,6 +4,7 @@ import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-
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";
@ -15,5 +16,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.GitHub]: registerGitHubSyncRouter,
[SecretSync.GCPSecretManager]: registerGcpSyncRouter,
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
[SecretSync.Databricks]: registerDatabricksSyncRouter
};

@ -18,6 +18,7 @@ import {
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";
@ -27,7 +28,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
GitHubSyncSchema,
GcpSyncSchema,
AzureKeyVaultSyncSchema,
AzureAppConfigurationSyncSchema
AzureAppConfigurationSyncSchema,
DatabricksSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@ -36,7 +38,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
GitHubSyncListItemSchema,
GcpSyncListItemSchema,
AzureKeyVaultSyncListItemSchema,
AzureAppConfigurationSyncListItemSchema
AzureAppConfigurationSyncListItemSchema,
DatabricksSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

@ -20,6 +20,7 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas";
@ -36,11 +37,12 @@ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = 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({
@ -618,7 +620,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),
@ -2029,6 +2031,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.UPDATE.secretPath),
mode: z
.nativeEnum(SecretUpdateMode)
.optional()
.default(SecretUpdateMode.FailOnNotFound)
.describe(RAW_SECRETS.UPDATE.mode),
secrets: z
.object({
secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName),
@ -2036,6 +2043,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.UPDATE.secretValue),
secretPath: z
.string()
.trim()
.transform(removeTrailingSlash)
.optional()
.describe(RAW_SECRETS.UPDATE.secretPath),
secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
@ -2072,7 +2085,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment,
projectSlug,
projectId: req.body.workspaceId,
secrets: inputSecrets
secrets: inputSecrets,
mode: req.body.mode
});
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
@ -2091,15 +2105,39 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
metadata: {
environment: req.body.environment,
secretPath: req.body.secretPath,
secrets: secrets.map((secret) => ({
secretId: secret.id,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
}))
secrets: secrets
.filter((el) => el.version > 1)
.map((secret) => ({
secretId: secret.id,
secretPath: secret.secretPath,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
}))
}
}
});
const createdSecrets = secrets.filter((el) => el.version === 1);
if (createdSecrets.length) {
await server.services.auditLog.createAuditLog({
projectId: secrets[0].workspace,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SECRETS,
metadata: {
environment: req.body.environment,
secretPath: req.body.secretPath,
secrets: createdSecrets.map((secret) => ({
secretId: secret.id,
secretPath: secret.secretPath,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
}))
}
}
});
}
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretUpdated,

@ -1,6 +1,7 @@
export enum AppConnection {
GitHub = "github",
AWS = "aws",
Databricks = "databricks",
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration"

@ -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 {
@ -33,11 +38,12 @@ import {
export const listAppConnectionOptions = () => {
return [
getAwsAppConnectionListItem(),
getAwsConnectionListItem(),
getGitHubConnectionListItem(),
getGcpAppConnectionListItem(),
getGcpConnectionListItem(),
getAzureKeyVaultConnectionListItem(),
getAzureAppConfigurationConnectionListItem()
getAzureAppConfigurationConnectionListItem(),
getDatabricksConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@ -90,6 +96,8 @@ 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:
@ -118,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}`);

@ -5,5 +5,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.GitHub]: "GitHub",
[AppConnection.GCP]: "GCP",
[AppConnection.AzureKeyVault]: "Azure Key Vault",
[AppConnection.AzureAppConfiguration]: "Azure App Configuration"
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
[AppConnection.Databricks]: "Databricks"
};

@ -23,6 +23,8 @@ 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";
@ -46,7 +48,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@ -365,6 +368,7 @@ export const appConnectionServiceFactory = ({
connectAppConnectionById,
listAvailableAppConnectionsForUser,
github: githubConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById)
gcp: gcpConnectionService(connectAppConnectionById),
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
};
};

@ -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,
@ -31,6 +37,7 @@ export type TAppConnection = { id: string } & (
| TGcpConnection
| TAzureKeyVaultConnection
| TAzureAppConfigurationConnection
| TDatabricksConnection
);
export type TAppConnectionInput = { id: string } & (
@ -39,6 +46,7 @@ export type TAppConnectionInput = { id: string } & (
| TGcpConnectionInput
| TAzureKeyVaultConnectionInput
| TAzureAppConfigurationConnectionInput
| TDatabricksConnectionInput
);
export type TCreateAppConnectionDTO = Pick<
@ -55,11 +63,13 @@ export type TAppConnectionConfig =
| TGitHubConnectionConfig
| TGcpConnectionConfig
| TAzureKeyVaultConnectionConfig
| TAzureAppConfigurationConnectionConfig;
| TAzureAppConfigurationConnectionConfig
| TDatabricksConnectionConfig;
export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials
| TValidateGitHubConnectionCredentials
| TValidateGcpConnectionCredentials
| TValidateAzureKeyVaultConnectionCredentials
| TValidateAzureAppConfigurationConnectionCredentials;
| TValidateAzureAppConfigurationConnectionCredentials
| TValidateDatabricksConnectionCredentials;

@ -9,7 +9,7 @@ import { AppConnection, AWSRegion } from "@app/services/app-connection/app-conne
import { AwsConnectionMethod } from "./aws-connection-enums";
import { TAwsConnectionConfig } from "./aws-connection-types";
export const getAwsAppConnectionListItem = () => {
export const getAwsConnectionListItem = () => {
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
return {

@ -0,0 +1,3 @@
export enum DatabricksConnectionMethod {
ServicePrincipal = "service-principal"
}

@ -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<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => {
const { data } = await request.post<TAuthorizeDatabricksConnection>(
`${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<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
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`
});
}
};

@ -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()
});

@ -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<TDatabricksConnection>;
const listDatabricksSecretScopes = async (
appConnection: TDatabricksConnection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const {
credentials: { workspaceUrl }
} = appConnection;
const accessToken = await getDatabricksConnectionAccessToken(appConnection, appConnectionDAL, kmsService);
const { data } = await request.get<TDatabricksListSecretScopesResponse>(
`${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<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
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
};
};

@ -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<typeof DatabricksConnectionSchema>;
export type TDatabricksConnectionInput = z.infer<typeof CreateDatabricksConnectionSchema> & {
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 } }[];
};

@ -0,0 +1,4 @@
export * from "./databricks-connection-enums";
export * from "./databricks-connection-fns";
export * from "./databricks-connection-schemas";
export * from "./databricks-connection-types";

@ -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,

@ -1,6 +1,7 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
@ -8,7 +9,7 @@ import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { AuthModeJwtTokenPayload } from "../auth/auth-type";
import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
import { TUserDALFactory } from "../user/user-dal";
import { TTokenDALFactory } from "./auth-token-dal";
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
@ -150,6 +151,40 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
const validateRefreshToken = async (refreshToken?: string) => {
const appCfg = getConfig();
if (!refreshToken)
throw new NotFoundError({
name: "AuthTokenNotFound",
message: "Failed to find refresh token"
});
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
throw new UnauthorizedError({
message: "The token provided is not a refresh token",
name: "InvalidToken"
});
const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId);
if (!tokenVersion)
throw new UnauthorizedError({
message: "Valid token version not found",
name: "InvalidToken"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
throw new UnauthorizedError({
message: "Token version mismatch",
name: "InvalidToken"
});
}
return { decodedToken, tokenVersion };
};
// to parse jwt identity in inject identity plugin
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
const session = await tokenDAL.findOneTokenSession({
@ -188,6 +223,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
clearTokenSessionById,
getTokenSessionByUser,
revokeAllMySessions,
validateRefreshToken,
fnValidateJwtIdentity,
getUserTokenSessionById
};

@ -35,6 +35,7 @@ export enum AuthMode {
export enum ActorType { // would extend to AWS, Azure, ...
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
KMIP_CLIENT = "kmipClient",
USER = "user", // userIdentity
SERVICE = "service",
IDENTITY = "identity",

@ -1,5 +1,7 @@
import { z } from "zod";
import { isValidIp } from "@app/lib/ip";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
@ -25,7 +27,7 @@ export const validateAltNamesField = z
if (data === "") return true;
// Split and validate each alt name
return data.split(", ").every((name) => {
return hostnameRegex.test(name) || z.string().email().safeParse(name).success;
return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name);
});
},
{

@ -40,3 +40,9 @@ export const isCertChainValid = async (certificates: x509.X509Certificate[]) =>
// chain.build() implicitly verifies the chain
return chainItems.length === certificates.length;
};
export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) =>
certificates
.map((cert) => cert.toString("pem"))
.join("\n")
.trim();

@ -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()

@ -93,6 +93,32 @@ export const kmskeyDALFactory = (db: TDbClient) => {
}
};
const findProjectCmeks = async (projectId: string, tx?: Knex) => {
try {
const result = await (tx || db.replicaNode())(TableName.KmsKey)
.where({
[`${TableName.KmsKey}.projectId` as "projectId"]: projectId,
[`${TableName.KmsKey}.isReserved` as "isReserved"]: false
})
.join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`)
.join(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
.select(selectAllTableCols(TableName.KmsKey))
.select(
db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms).as("internalKmsEncryptionAlgorithm"),
db.ref("version").withSchema(TableName.InternalKms).as("internalKmsVersion")
);
return result.map((entry) => ({
...KmsKeysSchema.parse(entry),
isActive: !entry.isDisabled,
algorithm: entry.internalKmsEncryptionAlgorithm,
version: entry.internalKmsVersion
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find project cmeks" });
}
};
const listCmeksByProjectId = async (
{
projectId,
@ -167,5 +193,5 @@ export const kmskeyDALFactory = (db: TDbClient) => {
}
};
return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName };
return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName, findProjectCmeks };
};

@ -1,8 +1,9 @@
import { Knex } from "knex";
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<typeof kmsRootConfigDALFactory>;

@ -37,6 +37,8 @@ import {
TEncryptWithKmsDataKeyDTO,
TEncryptWithKmsDTO,
TGenerateKMSDTO,
TGetKeyMaterialDTO,
TImportKeyMaterialDTO,
TUpdateProjectSecretManagerKmsKeyDTO
} from "./kms-types";
@ -325,6 +327,72 @@ export const kmsServiceFactory = ({
};
};
const getKeyMaterial = async ({ kmsId }: TGetKeyMaterialDTO) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) {
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
}
if (kmsDoc.isReserved) {
throw new BadRequestError({
message: "Cannot get key material for reserved key"
});
}
if (kmsDoc.externalKms) {
throw new BadRequestError({
message: "Cannot get key material for external key"
});
}
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return kmsKey;
};
const importKeyMaterial = async (
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
tx?: Knex
) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
if (key.byteLength !== expectedByteLength) {
throw new BadRequestError({
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
});
}
const encryptedKeyMaterial = cipher.encrypt(key, ROOT_ENCRYPTION_KEY);
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
const dbQuery = async (db: Knex) => {
const kmsDoc = await kmsDAL.create(
{
name: sanitizedName,
orgId,
isReserved,
projectId
},
db
);
await internalKmsDAL.create(
{
version: 1,
encryptedKey: encryptedKeyMaterial,
encryptionAlgorithm: algorithm,
kmsKeyId: kmsDoc.id
},
db
);
return kmsDoc;
};
if (tx) return dbQuery(tx);
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
return doc;
};
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) {
@ -944,6 +1012,8 @@ export const kmsServiceFactory = ({
getProjectKeyBackup,
loadProjectKeyBackup,
getKmsById,
createCipherPairWithDataKey
createCipherPairWithDataKey,
getKeyMaterial,
importKeyMaterial
};
};

@ -61,3 +61,15 @@ export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}
export type TGetKeyMaterialDTO = {
kmsId: string;
};
export type TImportKeyMaterialDTO = {
key: Buffer;
algorithm: SymmetricEncryption;
name?: string;
isReserved: boolean;
projectId: string;
orgId: string;
};

@ -10,14 +10,14 @@ import {
} from "@app/services/secret-sync/secret-sync-schemas";
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.REGION),
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
path: z
.string()
.trim()
.min(1, "Parameter Store Path required")
.max(2048, "Cannot exceed 2048 characters")
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format')
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.PATH)
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
});
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({

@ -272,22 +272,6 @@ export const AwsSecretsManagerSyncFns = {
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<TSecretMap> => {

@ -15,12 +15,12 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
z.object({
mappingBehavior: z
.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR)
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior)
}),
z.object({
mappingBehavior: z
.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne)
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR),
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior),
secretName: z
.string()
.regex(
@ -29,12 +29,12 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
)
.min(1, "Secret name is required")
.max(256, "Secret name cannot exceed 256 characters")
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.SECRET_NAME)
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName)
})
])
.and(
z.object({
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.REGION)
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.region)
})
);

@ -11,7 +11,7 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types";
type TAzureAppConfigurationSecretSyncFactoryDeps = {
type TAzureAppConfigurationSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
@ -22,10 +22,10 @@ interface AzureAppConfigKeyValue {
label?: string;
}
export const azureAppConfigurationSecretSyncFactory = ({
export const azureAppConfigurationSyncFactory = ({
kmsService,
appConnectionDAL
}: TAzureAppConfigurationSecretSyncFactoryDeps) => {
}: TAzureAppConfigurationSyncFactoryDeps) => {
const $getCompleteAzureAppConfigValues = async (accessToken: string, baseURL: string, url: string) => {
let result: AzureAppConfigKeyValue[] = [];
let currentUrl = url;

@ -14,8 +14,8 @@ const AzureAppConfigurationSyncDestinationConfigSchema = z.object({
configurationUrl: z
.string()
.min(1, "App Configuration URL required")
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.CONFIGURATION_URL),
label: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.LABEL)
.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 };

@ -10,15 +10,12 @@ 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 TAzureKeyVaultSecretSyncFactoryDeps = {
type TAzureKeyVaultSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export const azureKeyVaultSecretSyncFactory = ({
kmsService,
appConnectionDAL
}: TAzureKeyVaultSecretSyncFactoryDeps) => {
export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultSyncFactoryDeps) => {
const $getAzureKeyVaultSecrets = async (accessToken: string, vaultBaseUrl: string) => {
const paginateAzureKeyVaultSecrets = async () => {
let result: GetAzureKeyVaultSecret[] = [];

@ -15,7 +15,7 @@ const AzureKeyVaultSyncDestinationConfigSchema = z.object({
.string()
.url("Invalid vault base URL format")
.min(1, "Vault base URL required")
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_KEY_VAULT.VAULT_BASE_URL)
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_KEY_VAULT.vaultBaseUrl)
});
const AzureKeyVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };

@ -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
};

@ -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<TAppConnectionDALFactory, "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
const DATABRICKS_SCOPE_SECRET_LIMIT = 1000;
const listDatabricksSecrets = async ({ workspaceUrl, scope, accessToken }: TDatabricksListSecretKeys) => {
const { data } = await request.get<TDatabricksListSecretKeysResponse>(
`${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
};
};

@ -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)
});

@ -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<typeof DatabricksSyncSchema>;
export type TDatabricksSyncInput = z.infer<typeof CreateDatabricksSyncSchema>;
export type TDatabricksSyncListItem = z.infer<typeof DatabricksSyncListItemSchema>;
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;

@ -0,0 +1,4 @@
export * from "./databricks-sync-constants";
export * from "./databricks-sync-fns";
export * from "./databricks-sync-schemas";
export * from "./databricks-sync-types";

@ -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({

@ -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) => {

@ -4,7 +4,8 @@ export enum SecretSync {
GitHub = "github",
GCPSecretManager = "gcp-secret-manager",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration"
AzureAppConfiguration = "azure-app-configuration",
Databricks = "databricks"
}
export enum SecretSyncInitialSyncBehavior {

@ -8,6 +8,7 @@ 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";
@ -19,11 +20,8 @@ import {
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import {
AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
azureAppConfigurationSecretSyncFactory
} from "./azure-app-configuration";
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSecretSyncFactory } from "./azure-key-vault";
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";
@ -33,7 +31,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.GitHub]: GITHUB_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.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@ -41,7 +40,7 @@ export const listSecretSyncOptions = () => {
};
type TSyncSecretDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
@ -103,12 +102,17 @@ export const SecretSyncFns = {
case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.AzureKeyVault:
return azureKeyVaultSecretSyncFactory({
return azureKeyVaultSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSecretSyncFactory({
return azureAppConfigurationSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
case SecretSync.Databricks:
return databricksSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
@ -137,17 +141,22 @@ export const SecretSyncFns = {
secretMap = await GcpSyncFns.getSecrets(secretSync);
break;
case SecretSync.AzureKeyVault:
secretMap = await azureKeyVaultSecretSyncFactory({
secretMap = await azureKeyVaultSyncFactory({
appConnectionDAL,
kmsService
}).getSecrets(secretSync);
break;
case SecretSync.AzureAppConfiguration:
secretMap = await azureAppConfigurationSecretSyncFactory({
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}`
@ -174,12 +183,17 @@ export const SecretSyncFns = {
case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.AzureKeyVault:
return azureKeyVaultSecretSyncFactory({
return azureKeyVaultSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSecretSyncFactory({
return azureAppConfigurationSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
case SecretSync.Databricks:
return databricksSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);

@ -7,7 +7,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.GitHub]: "GitHub",
[SecretSync.GCPSecretManager]: "GCP Secret Manager",
[SecretSync.AzureKeyVault]: "Azure Key Vault",
[SecretSync.AzureAppConfiguration]: "Azure App Configuration"
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
[SecretSync.Databricks]: "Databricks"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@ -16,5 +17,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.GitHub]: AppConnection.GitHub,
[SecretSync.GCPSecretManager]: AppConnection.GCP,
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
[SecretSync.Databricks]: AppConnection.Databricks
};

@ -64,7 +64,7 @@ export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
type TSecretSyncQueueFactoryDep = {
queueService: Pick<TQueueServiceFactory, "queue" | "start">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
folderDAL: TSecretFolderDALFactory;
secretV2BridgeDAL: Pick<

@ -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()

@ -8,6 +8,12 @@ import {
TAwsSecretsManagerSyncListItem,
TAwsSecretsManagerSyncWithCredentials
} from "@app/services/secret-sync/aws-secrets-manager";
import {
TDatabricksSync,
TDatabricksSyncInput,
TDatabricksSyncListItem,
TDatabricksSyncWithCredentials
} from "@app/services/secret-sync/databricks";
import {
TGitHubSync,
TGitHubSyncInput,
@ -43,7 +49,8 @@ export type TSecretSync =
| TGitHubSync
| TGcpSync
| TAzureKeyVaultSync
| TAzureAppConfigurationSync;
| TAzureAppConfigurationSync
| TDatabricksSync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@ -51,7 +58,8 @@ export type TSecretSyncWithCredentials =
| TGitHubSyncWithCredentials
| TGcpSyncWithCredentials
| TAzureKeyVaultSyncWithCredentials
| TAzureAppConfigurationSyncWithCredentials;
| TAzureAppConfigurationSyncWithCredentials
| TDatabricksSyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@ -59,7 +67,8 @@ export type TSecretSyncInput =
| TGitHubSyncInput
| TGcpSyncInput
| TAzureKeyVaultSyncInput
| TAzureAppConfigurationSyncInput;
| TAzureAppConfigurationSyncInput
| TDatabricksSyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@ -67,7 +76,8 @@ export type TSecretSyncListItem =
| TGitHubSyncListItem
| TGcpSyncListItem
| TAzureKeyVaultSyncListItem
| TAzureAppConfigurationSyncListItem;
| TAzureAppConfigurationSyncListItem
| TDatabricksSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

@ -1,7 +1,15 @@
import { ForbiddenError, PureAbility, subject } from "@casl/ability";
import { Knex } from "knex";
import { z } from "zod";
import { ActionProjectType, ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas";
import {
ActionProjectType,
ProjectMembershipRole,
SecretsV2Schema,
SecretType,
TableName,
TSecretsV2
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -36,6 +44,7 @@ import {
} from "./secret-v2-bridge-fns";
import {
SecretOperations,
SecretUpdateMode,
TBackFillSecretReferencesDTO,
TCreateManySecretDTO,
TCreateSecretDTO,
@ -103,12 +112,13 @@ export const secretV2BridgeServiceFactory = ({
const $validateSecretReferences = async (
projectId: string,
permission: PureAbility,
references: ReturnType<typeof getAllSecretReferences>["nestedReferences"]
references: ReturnType<typeof getAllSecretReferences>["nestedReferences"],
tx?: Knex
) => {
if (!references.length) return;
const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment)));
const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs);
const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx);
if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length)
throw new BadRequestError({
message: `Referenced environment not found. Missing ${diff(
@ -122,36 +132,41 @@ export const secretV2BridgeServiceFactory = ({
references.map((el) => ({
secretPath: el.secretPath,
envId: referencesEnvironmentGroupBySlug[el.environment][0].id
}))
})),
tx
);
const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`);
const referredSecrets = await secretDAL.find({
$complex: {
operator: "or",
value: references.map((el) => {
const folderId =
referencesFolderGroupByPath[`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`][0]
?.id;
if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` });
const referredSecrets = await secretDAL.find(
{
$complex: {
operator: "or",
value: references.map((el) => {
const folderId =
referencesFolderGroupByPath[
`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`
][0]?.id;
if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` });
return {
operator: "and",
value: [
{
operator: "eq",
field: "folderId",
value: folderId
},
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
}
]
};
})
}
});
return {
operator: "and",
value: [
{
operator: "eq",
field: "folderId",
value: folderId
},
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
}
]
};
})
}
},
{ tx }
);
if (
referredSecrets.length !==
@ -1245,8 +1260,9 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
environment,
projectId,
secretPath,
secrets: inputSecrets
secretPath: defaultSecretPath = "/",
secrets: inputSecrets,
mode: updateMode
}: TUpdateManySecretDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
@ -1257,196 +1273,280 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
const secretsToUpdateGroupByPath = groupBy(inputSecrets, (el) => el.secretPath || defaultSecretPath);
const projectEnvironment = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!projectEnvironment) {
throw new NotFoundError({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`,
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`
});
}
const folders = await folderDAL.findByManySecretPath(
Object.keys(secretsToUpdateGroupByPath).map((el) => ({ envId: projectEnvironment.id, secretPath: el }))
);
if (folders.length !== Object.keys(secretsToUpdateGroupByPath).length)
throw new NotFoundError({
message: `Folder with path '${null}' in environment with slug '${environment}' not found`,
name: "UpdateManySecret"
});
const folderId = folder.id;
const secretsToUpdate = await secretDAL.find({
folderId,
$complex: {
operator: "and",
value: [
{
operator: "or",
value: inputSecrets.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
});
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) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
);
});
// get all tags
const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : [];
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" });
const tagsGroupByID = groupBy(tags, (i) => i.id);
// check again to avoid non authorized tags are removed
inputSecrets.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.secretKey,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
// now find any secret that needs to update its name
// same process as above
const secretsWithNewName = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName));
if (secretsWithNewName.length) {
const secrets = await secretDAL.find({
folderId,
$complex: {
operator: "and",
value: [
{
operator: "or",
value: secretsWithNewName.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
});
if (secrets.length)
throw new BadRequestError({
message: `Secret with new name already exists: ${secretsWithNewName.map((el) => el.newSecretName).join(",")}`
});
secretsWithNewName.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.newSecretName as string,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
}
// now get all secret references made and validate the permission
const secretReferencesGroupByInputSecretKey: Record<string, ReturnType<typeof getAllSecretReferences>> = {};
const secretReferences: TSecretReference[] = [];
inputSecrets.forEach((el) => {
if (el.secretValue) {
const references = getAllSecretReferences(el.secretValue);
secretReferencesGroupByInputSecretKey[el.secretKey] = references;
secretReferences.push(...references.nestedReferences);
references.localReferences.forEach((localRefKey) => {
secretReferences.push({ secretKey: localRefKey, secretPath, environment });
});
}
});
await $validateSecretReferences(projectId, permission, secretReferences);
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
const secrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: inputSecrets.map((el) => {
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
const encryptedValue =
typeof el.secretValue !== "undefined"
? {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob,
references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences
}
: {};
const updatedSecrets: Array<TSecretsV2 & { secretPath: string }> = [];
await secretDAL.transaction(async (tx) => {
for await (const folder of folders) {
if (!folder) throw new NotFoundError({ message: "Folder not found" });
return {
filter: { id: originalSecret.id, type: SecretType.Shared },
data: {
reminderRepeatDays: el.secretReminderRepeatDays,
encryptedComment: setKnexStringValue(
el.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
reminderNote: el.secretReminderNote,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.newSecretName || el.secretKey,
tags: el.tagIds,
secretMetadata: el.secretMetadata,
...encryptedValue
const folderId = folder.id;
const secretPath = folder.path;
let secretsToUpdate = secretsToUpdateGroupByPath[secretPath];
const secretsToUpdateInDB = await secretDAL.find(
{
folderId,
$complex: {
operator: "and",
value: [
{
operator: "or",
value: secretsToUpdate.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
};
}),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
resourceMetadataDAL
})
);
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
actor,
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
},
{ tx }
);
if (secretsToUpdateInDB.length !== secretsToUpdate.length && updateMode === SecretUpdateMode.FailOnNotFound)
throw new NotFoundError({
message: `Secret does not exist: ${diff(
secretsToUpdate.map((el) => el.secretKey),
secretsToUpdateInDB.map((el) => el.key)
).join(", ")} in path ${folder.path}`
});
const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdateInDB, (i) => i.key);
const secretsToCreate = secretsToUpdate.filter((el) => !secretsToUpdateInDBGroupedByKey?.[el.secretKey]);
secretsToUpdate = secretsToUpdate.filter((el) => secretsToUpdateInDBGroupedByKey?.[el.secretKey]);
secretsToUpdateInDB.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
);
});
// get all tags
const sanitizedTagIds = secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds);
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds, tx) : [];
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" });
const tagsGroupByID = groupBy(tags, (i) => i.id);
// check create permission allowed in upsert mode
if (updateMode === SecretUpdateMode.Upsert) {
secretsToCreate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.secretKey,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
}
// check again to avoid non authorized tags are removed
secretsToUpdate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.secretKey,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
// now find any secret that needs to update its name
// same process as above
const secretsWithNewName = secretsToUpdate.filter(({ newSecretName }) => Boolean(newSecretName));
if (secretsWithNewName.length) {
const secrets = await secretDAL.find(
{
folderId,
$complex: {
operator: "and",
value: [
{
operator: "or",
value: secretsWithNewName.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
},
{ tx }
);
if (secrets.length)
throw new BadRequestError({
message: `Secret with new name already exists: ${secretsWithNewName
.map((el) => el.newSecretName)
.join(", ")}`
});
secretsWithNewName.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.newSecretName as string,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
}
// now get all secret references made and validate the permission
const secretReferencesGroupByInputSecretKey: Record<string, ReturnType<typeof getAllSecretReferences>> = {};
const secretReferences: TSecretReference[] = [];
secretsToUpdate.concat(SecretUpdateMode.Upsert === updateMode ? secretsToCreate : []).forEach((el) => {
if (el.secretValue) {
const references = getAllSecretReferences(el.secretValue);
secretReferencesGroupByInputSecretKey[el.secretKey] = references;
secretReferences.push(...references.nestedReferences);
references.localReferences.forEach((localRefKey) => {
secretReferences.push({ secretKey: localRefKey, secretPath, environment });
});
}
});
await $validateSecretReferences(projectId, permission, secretReferences, tx);
const bulkUpdatedSecrets = await fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: secretsToUpdate.map((el) => {
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
const encryptedValue =
typeof el.secretValue !== "undefined"
? {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob,
references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences
}
: {};
return {
filter: { id: originalSecret.id, type: SecretType.Shared },
data: {
reminderRepeatDays: el.secretReminderRepeatDays,
encryptedComment: setKnexStringValue(
el.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
reminderNote: el.secretReminderNote,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.newSecretName || el.secretKey,
tags: el.tagIds,
secretMetadata: el.secretMetadata,
...encryptedValue
}
};
}),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
resourceMetadataDAL
});
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
if (updateMode === SecretUpdateMode.Upsert) {
const bulkInsertedSecrets = await fnSecretBulkInsert({
inputSecrets: secretsToCreate.map((el) => {
const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences;
return {
version: 1,
encryptedComment: setKnexStringValue(
el.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
encryptedValue: el.secretValue
? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
: undefined,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.secretKey,
tagIds: el.tagIds,
references,
secretMetadata: el.secretMetadata,
type: SecretType.Shared
};
}),
folderId,
orgId: actorOrgId,
secretDAL,
resourceMetadataDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
});
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
}
}
});
return secrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, {
await Promise.allSettled(folders.map((el) => (el?.id ? snapshotService.performSnapshot(el.id) : undefined)));
await Promise.allSettled(
folders.map((el) =>
el
? secretQueueService.syncSecrets({
actor,
actorId,
secretPath: el.path,
projectId,
orgId: actorOrgId,
environmentSlug: environment
})
: undefined
)
);
return updatedSecrets.map((el) =>
reshapeBridgeSecret(projectId, environment, el.secretPath, {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""

@ -23,6 +23,12 @@ export type TSecretReferenceDTO = {
secretKey: string;
};
export enum SecretUpdateMode {
Ignore = "ignore",
Upsert = "upsert",
FailOnNotFound = "failOnNotFound"
}
export type TGetSecretsDTO = {
expandSecretReferences?: boolean;
path: string;
@ -113,6 +119,7 @@ export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretPath: string;
projectId: string;
environment: string;
mode: SecretUpdateMode;
secrets: {
secretKey: string;
newSecretName?: string;
@ -123,6 +130,7 @@ export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretMetadata?: ResourceMetadataDTO;
secretPath?: string;
}[];
};

@ -30,7 +30,10 @@ import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { OrgServiceActor } from "@app/lib/types";
import { TGetSecretsRawByFolderMappingsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import {
SecretUpdateMode,
TGetSecretsRawByFolderMappingsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal";
@ -2012,6 +2015,7 @@ export const secretServiceFactory = ({
actorOrgId,
actorAuthMethod,
secretPath,
mode = SecretUpdateMode.FailOnNotFound,
secrets: inputSecrets = []
}: TUpdateManySecretRawDTO) => {
if (!projectSlug && !optionalProjectId)
@ -2076,7 +2080,8 @@ export const secretServiceFactory = ({
actorOrgId,
actor,
actorId,
secrets: inputSecrets
secrets: inputSecrets,
mode
});
return { type: SecretProtectionType.Direct as const, secrets };
}

@ -17,6 +17,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
@ -274,6 +275,7 @@ export type TUpdateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
projectId?: string;
projectSlug?: string;
environment: string;
mode: SecretUpdateMode;
secrets: {
secretKey: string;
newSecretName?: string;

@ -10,7 +10,8 @@ require (
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.4.7
github.com/infisical/go-sdk v0.4.8
github.com/infisical/infisical-kmip v0.3.5
github.com/mattn/go-isatty v0.0.20
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
github.com/muesli/mango-cobra v1.2.0
@ -65,6 +66,9 @@ require (
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/gosimple/slug v1.15.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
@ -77,6 +81,7 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
@ -91,12 +96,12 @@ require (
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/api v0.188.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
@ -108,7 +113,7 @@ require (
require (
github.com/fatih/color v1.17.0
github.com/go-resty/resty/v2 v2.13.1
github.com/go-resty/resty/v2 v2.16.5
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/manifoldco/promptui v0.9.0

@ -152,8 +152,8 @@ github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02E
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o=
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -237,6 +237,10 @@ github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBY
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
@ -255,6 +259,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@ -265,8 +271,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/infisical/go-sdk v0.4.7 h1:+cxIdDfciMh0Syxbxbqjhvz9/ShnN1equ2zqlVQYGtw=
github.com/infisical/go-sdk v0.4.7/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
github.com/infisical/go-sdk v0.4.8 h1:aphRnaauC5//PkP1ZbY9RSK2RiT1LjPS5o4CbX0x5OQ=
github.com/infisical/go-sdk v0.4.8/go.mod h1:bMO9xSaBeXkDBhTIM4FkkREAfw2V8mv5Bm7lvo4+uDk=
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -340,6 +348,7 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -413,7 +422,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@ -448,11 +456,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -490,8 +495,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -530,13 +533,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -562,8 +560,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -612,22 +608,11 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -639,17 +624,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -702,8 +683,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

103
cli/packages/cmd/kmip.go Normal file

@ -0,0 +1,103 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"fmt"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/util"
kmip "github.com/infisical/infisical-kmip"
"github.com/spf13/cobra"
)
var kmipCmd = &cobra.Command{
Example: `infisical kmip`,
Short: "Used to manage KMIP servers",
Use: "kmip",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
}
var kmipStartCmd = &cobra.Command{
Example: `infisical kmip start`,
Short: "Used to start a KMIP server",
Use: "start",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: startKmipServer,
}
func startKmipServer(cmd *cobra.Command, args []string) {
listenAddr, err := cmd.Flags().GetString("listen-address")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
identityAuthMethod, err := cmd.Flags().GetString("identity-auth-method")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
authMethodValid, strategy := util.IsAuthMethodValid(identityAuthMethod, false)
if !authMethodValid {
util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", identityAuthMethod))
}
var identityClientId string
var identityClientSecret string
if strategy == util.AuthStrategy.UNIVERSAL_AUTH {
identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
if err != nil {
util.HandleError(err, "Unable to parse identity client ID")
}
identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME)
if err != nil {
util.HandleError(err, "Unable to parse identity client secret")
}
} else {
util.PrintErrorMessageAndExit(fmt.Sprintf("Unsupported login method: %s", identityAuthMethod))
}
serverName, err := cmd.Flags().GetString("server-name")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
certificateTTL, err := cmd.Flags().GetString("certificate-ttl")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
hostnamesOrIps, err := cmd.Flags().GetString("hostnames-or-ips")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
kmip.StartServer(kmip.ServerConfig{
Addr: listenAddr,
InfisicalBaseAPIURL: config.INFISICAL_URL,
IdentityClientId: identityClientId,
IdentityClientSecret: identityClientSecret,
ServerName: serverName,
CertificateTTL: certificateTTL,
HostnamesOrIps: hostnamesOrIps,
})
}
func init() {
kmipStartCmd.Flags().String("listen-address", "localhost:5696", "The address for the KMIP server to listen on. Defaults to localhost:5696")
kmipStartCmd.Flags().String("identity-auth-method", string(util.AuthStrategy.UNIVERSAL_AUTH), "The auth method to use for authenticating the machine identity. Defaults to universal-auth.")
kmipStartCmd.Flags().String("identity-client-id", "", "Universal auth client ID of machine identity")
kmipStartCmd.Flags().String("identity-client-secret", "", "Universal auth client secret of machine identity")
kmipStartCmd.Flags().String("server-name", "kmip-server", "The name of the KMIP server")
kmipStartCmd.Flags().String("certificate-ttl", "1y", "The TTL duration for the server certificate")
kmipStartCmd.Flags().String("hostnames-or-ips", "", "Comma-separated list of hostnames or IPs")
kmipCmd.AddCommand(kmipStartCmd)
rootCmd.AddCommand(kmipCmd)
}

@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/databricks/available"
---

@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/databricks"
---

Some files were not shown because too many files have changed in this diff Show More