mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-14 08:08:30 +00:00
Compare commits
155 Commits
misc/add-d
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
fe58508136 | ||
|
dd52f4d7e0 | ||
|
aa7ad9a8c8 | ||
|
85a716628b | ||
|
4b0e5fa05b | ||
|
27fdf68e42 | ||
|
9a5bc33517 | ||
|
0fecbad43c | ||
|
511a81a464 | ||
|
70f5f21e7f | ||
|
b5b0d42dd5 | ||
|
d888d990d0 | ||
|
1cbab41609 | ||
|
49b5b488ef | ||
|
bb59e04c28 | ||
|
46b08dccd1 | ||
|
53ca8d7161 | ||
|
e19c3630d9 | ||
|
071dab723a | ||
|
1ce155e2fd | ||
|
2ed05c26e8 | ||
|
9e0fdb10b1 | ||
|
5c40347c52 | ||
|
edf375ca48 | ||
|
264177638f | ||
|
230b44fca1 | ||
|
3d02feaad9 | ||
|
77dd768a38 | ||
|
eb11efcafa | ||
|
8522420e7f | ||
|
81331ec4d1 | ||
|
f15491d102 | ||
|
4d4547015e | ||
|
06cd496ab3 | ||
|
4119478704 | ||
|
700efc9b6d | ||
|
b76ee9cc49 | ||
|
c498178923 | ||
|
8bb68f9889 | ||
|
1c121ec30d | ||
|
956d97eda2 | ||
|
e877a4c9e9 | ||
|
ee9a7cd5a1 | ||
|
a84dddaf6f | ||
|
8cbfeffe4c | ||
|
2084539f61 | ||
|
9baab63b29 | ||
|
34cf47a5eb | ||
|
b90c6cf3fc | ||
|
68374a17f0 | ||
|
993eb4d239 | ||
|
2382937385 | ||
|
05af70161a | ||
|
2940300164 | ||
|
9356ab7cbc | ||
|
bbc94da522 | ||
|
8a241771ec | ||
|
ed5c18b5ac | ||
|
1f23515aac | ||
|
d01cb282f9 | ||
|
6dc085b970 | ||
|
63dc9ec35d | ||
|
1d083befe4 | ||
|
c01e29b932 | ||
|
3aed79071b | ||
|
140fa49871 | ||
|
03a3e80082 | ||
|
5a114586dc | ||
|
20ebfcefaa | ||
|
bfcfffbabf | ||
|
210bd220e5 | ||
|
7be2a10631 | ||
|
5753eb7d77 | ||
|
cb86aa40fa | ||
|
1131143a71 | ||
|
728c3f56a7 | ||
|
a50b8120fd | ||
|
f1ee53d417 | ||
|
229ad79f49 | ||
|
d7dbd01ecf | ||
|
026fd21fd4 | ||
|
9b9c1a52b3 | ||
|
98aa424e2e | ||
|
2cd5df1ab3 | ||
|
e0d863e06e | ||
|
d991af557b | ||
|
ae54d04357 | ||
|
fa590ba697 | ||
|
9899864133 | ||
|
06715b1b58 | ||
|
038f43b769 | ||
|
35d7881613 | ||
|
b444908022 | ||
|
3f9a793578 | ||
|
479d6445a7 | ||
|
bf5e8d8c8b | ||
|
99aa567a6f | ||
|
1da2896bb0 | ||
|
423a2f38ea | ||
|
db0a72f7b4 | ||
|
4a202d180a | ||
|
33103f1e95 | ||
|
ce8a4bc50e | ||
|
141a821091 | ||
|
b3dd5410d7 | ||
|
74574c6c29 | ||
|
4f32756951 | ||
|
961fe09a6e | ||
|
eb4816fd29 | ||
|
5ab853d3e6 | ||
|
0e073cc9fc | ||
|
715bb447e6 | ||
|
c2f2a038ad | ||
|
433b1a49f0 | ||
|
5671cd5cef | ||
|
b8f04d6738 | ||
|
b0b255461d | ||
|
c2f2dc1e72 | ||
|
0ee1b425df | ||
|
46e72e9fba | ||
|
06fc4e955d | ||
|
18c8fc66ee | ||
|
ece294c483 | ||
|
9a712b5c85 | ||
|
1ec427053b | ||
|
6c636415bb | ||
|
e323cb4630 | ||
|
e87a1bd402 | ||
|
3b09173bb1 | ||
|
d957419b94 | ||
|
ec9897d561 | ||
|
4d41513abf | ||
|
9fc9f69fc9 | ||
|
cd333a7923 | ||
|
e11fdf8f3a | ||
|
4725108319 | ||
|
715441908b | ||
|
3f190426fe | ||
|
419dd37d03 | ||
|
f00a54ed54 | ||
|
a25c25434c | ||
|
4f72d09458 | ||
|
08baf02ef0 | ||
|
577c81be65 | ||
|
064322936b | ||
|
7634fc94a6 | ||
|
d82b06c72b | ||
|
3d072c2f48 | ||
|
82b828c10e | ||
|
5e7ad5614d | ||
|
f825a62af2 | ||
|
90bf8f800b | ||
|
766c1242fd | ||
|
dbabb4f964 | ||
|
4b9f409ea5 |
253
.github/workflows/release_build_infisical_cli.yml
vendored
253
.github/workflows/release_build_infisical_cli.yml
vendored
@@ -1,132 +1,147 @@
|
|||||||
name: Build and release CLI
|
name: Build and release CLI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
push:
|
push:
|
||||||
# run only against tags
|
# run only against tags
|
||||||
tags:
|
tags:
|
||||||
- "infisical-cli/v*.*.*"
|
- "infisical-cli/v*.*.*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cli-integration-tests:
|
cli-integration-tests:
|
||||||
name: Run tests before deployment
|
name: Run tests before deployment
|
||||||
uses: ./.github/workflows/run-cli-tests.yml
|
uses: ./.github/workflows/run-cli-tests.yml
|
||||||
secrets:
|
secrets:
|
||||||
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
||||||
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
||||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||||
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||||
|
|
||||||
npm-release:
|
npm-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
working-directory: ./npm
|
||||||
|
needs:
|
||||||
|
- cli-integration-tests
|
||||||
|
- goreleaser
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract version
|
||||||
|
run: |
|
||||||
|
VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//')
|
||||||
|
echo "Version extracted: $VERSION"
|
||||||
|
echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Print version
|
||||||
|
run: echo ${{ env.CLI_VERSION }}
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: ./npm/package-lock.json
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: npm install --ignore-scripts
|
||||||
|
|
||||||
|
- name: Set NPM version
|
||||||
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version
|
||||||
|
|
||||||
|
- name: Setup NPM
|
||||||
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: |
|
||||||
|
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
|
||||||
|
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
|
||||||
|
|
||||||
|
echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc
|
||||||
|
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||||
env:
|
env:
|
||||||
working-directory: ./npm
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
needs:
|
|
||||||
- cli-integration-tests
|
|
||||||
- goreleaser
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Extract version
|
- name: Pack NPM
|
||||||
run: |
|
working-directory: ${{ env.working-directory }}
|
||||||
VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//')
|
run: npm pack
|
||||||
echo "Version extracted: $VERSION"
|
|
||||||
echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Print version
|
- name: Publish NPM
|
||||||
run: echo ${{ env.CLI_VERSION }}
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Setup Node
|
goreleaser:
|
||||||
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
runs-on: ubuntu-latest
|
||||||
with:
|
needs: [cli-integration-tests]
|
||||||
node-version: 20
|
steps:
|
||||||
cache: "npm"
|
- uses: actions/checkout@v3
|
||||||
cache-dependency-path: ./npm/package-lock.json
|
with:
|
||||||
- name: Install dependencies
|
fetch-depth: 0
|
||||||
working-directory: ${{ env.working-directory }}
|
- name: 🐋 Login to Docker Hub
|
||||||
run: npm install --ignore-scripts
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
- name: Set NPM version
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
working-directory: ${{ env.working-directory }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Setup NPM
|
- run: git fetch --force --tags
|
||||||
working-directory: ${{ env.working-directory }}
|
- run: echo "Ref name ${{github.ref_name}}"
|
||||||
run: |
|
- uses: actions/setup-go@v3
|
||||||
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
|
with:
|
||||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
|
go-version: ">=1.19.3"
|
||||||
|
cache: true
|
||||||
echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc
|
cache-dependency-path: cli/go.sum
|
||||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
- name: Setup for libssl1.0-dev
|
||||||
env:
|
run: |
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||||
|
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
|
||||||
- name: Pack NPM
|
sudo apt update
|
||||||
working-directory: ${{ env.working-directory }}
|
sudo apt-get install -y libssl1.0-dev
|
||||||
run: npm pack
|
- name: OSXCross for CGO Support
|
||||||
|
run: |
|
||||||
- name: Publish NPM
|
mkdir ../../osxcross
|
||||||
working-directory: ${{ env.working-directory }}
|
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||||
run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/
|
- uses: goreleaser/goreleaser-action@v4
|
||||||
env:
|
with:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
distribution: goreleaser-pro
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
version: v1.26.2-pro
|
||||||
|
args: release --clean
|
||||||
goreleaser:
|
env:
|
||||||
runs-on: ubuntu-latest
|
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||||
needs: [cli-integration-tests]
|
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
||||||
steps:
|
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||||
- uses: actions/checkout@v3
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
with:
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
fetch-depth: 0
|
- uses: actions/setup-python@v4
|
||||||
- name: 🐋 Login to Docker Hub
|
- run: pip install --upgrade cloudsmith-cli
|
||||||
uses: docker/login-action@v2
|
- uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
ruby-version: "3.3" # Not needed with a .ruby-version, .tool-versions or mise.toml
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
- name: 🔧 Set up Docker Buildx
|
- name: Install deb-s3
|
||||||
uses: docker/setup-buildx-action@v2
|
run: gem install deb-s3
|
||||||
- run: git fetch --force --tags
|
- name: Configure GPG Key
|
||||||
- run: echo "Ref name ${{github.ref_name}}"
|
run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --batch --import
|
||||||
- uses: actions/setup-go@v3
|
env:
|
||||||
with:
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||||
go-version: ">=1.19.3"
|
GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
|
||||||
cache: true
|
- name: Publish to CloudSmith
|
||||||
cache-dependency-path: cli/go.sum
|
run: sh cli/upload_to_cloudsmith.sh
|
||||||
- name: Setup for libssl1.0-dev
|
env:
|
||||||
run: |
|
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||||
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }}
|
||||||
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
|
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
|
||||||
sudo apt update
|
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
|
||||||
sudo apt-get install -y libssl1.0-dev
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
|
||||||
- name: OSXCross for CGO Support
|
|
||||||
run: |
|
|
||||||
mkdir ../../osxcross
|
|
||||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
|
||||||
- uses: goreleaser/goreleaser-action@v4
|
|
||||||
with:
|
|
||||||
distribution: goreleaser-pro
|
|
||||||
version: v1.26.2-pro
|
|
||||||
args: release --clean
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
|
||||||
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
|
||||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
|
||||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
- run: pip install --upgrade cloudsmith-cli
|
|
||||||
- name: Publish to CloudSmith
|
|
||||||
run: sh cli/upload_to_cloudsmith.sh
|
|
||||||
env:
|
|
||||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
|
||||||
|
@@ -162,6 +162,24 @@ scoop:
|
|||||||
description: "The official Infisical CLI"
|
description: "The official Infisical CLI"
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
||||||
|
winget:
|
||||||
|
- name: infisical
|
||||||
|
publisher: infisical
|
||||||
|
license: MIT
|
||||||
|
homepage: https://infisical.com
|
||||||
|
short_description: "The official Infisical CLI"
|
||||||
|
repository:
|
||||||
|
owner: infisical
|
||||||
|
name: winget-pkgs
|
||||||
|
branch: "infisical-{{.Version}}"
|
||||||
|
pull_request:
|
||||||
|
enabled: true
|
||||||
|
draft: false
|
||||||
|
base:
|
||||||
|
owner: microsoft
|
||||||
|
name: winget-pkgs
|
||||||
|
branch: master
|
||||||
|
|
||||||
aurs:
|
aurs:
|
||||||
- name: infisical-bin
|
- name: infisical-bin
|
||||||
homepage: "https://infisical.com"
|
homepage: "https://infisical.com"
|
||||||
|
@@ -14,3 +14,11 @@ docs/self-hosting/guides/automated-bootstrapping.mdx:jwt:74
|
|||||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72
|
||||||
k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11
|
k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11
|
||||||
k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52
|
k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52
|
||||||
|
backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts:generic-api-key:125
|
||||||
|
frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx:generic-api-key:67
|
||||||
|
frontend/src/components/secret-rotations-v2/RotateSecretRotationV2Modal.tsx:generic-api-key:14
|
||||||
|
frontend/src/components/secret-rotations-v2/SecretRotationV2StatusBadge.tsx:generic-api-key:11
|
||||||
|
frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx:generic-api-key:23
|
||||||
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
||||||
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
||||||
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||||
|
@@ -11,6 +11,7 @@ export const mockQueue = (): TQueueServiceFactory => {
|
|||||||
job[name] = jobData;
|
job[name] = jobData;
|
||||||
},
|
},
|
||||||
queuePg: async () => {},
|
queuePg: async () => {},
|
||||||
|
schedulePg: async () => {},
|
||||||
initialize: async () => {},
|
initialize: async () => {},
|
||||||
shutdown: async () => undefined,
|
shutdown: async () => undefined,
|
||||||
stopRepeatableJob: async () => true,
|
stopRepeatableJob: async () => true,
|
||||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@@ -33,10 +33,12 @@ import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
|||||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
|
import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
|
||||||
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||||
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
|
import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
@@ -205,6 +207,7 @@ declare module "fastify" {
|
|||||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||||
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
||||||
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
||||||
|
sshHost: TSshHostServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
certificateEst: TCertificateEstServiceFactory;
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
@@ -237,6 +240,7 @@ declare module "fastify" {
|
|||||||
kmip: TKmipServiceFactory;
|
kmip: TKmipServiceFactory;
|
||||||
kmipOperation: TKmipOperationServiceFactory;
|
kmipOperation: TKmipOperationServiceFactory;
|
||||||
gateway: TGatewayServiceFactory;
|
gateway: TGatewayServiceFactory;
|
||||||
|
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
80
backend/src/@types/knex.d.ts
vendored
80
backend/src/@types/knex.d.ts
vendored
@@ -17,6 +17,9 @@ import {
|
|||||||
TApiKeys,
|
TApiKeys,
|
||||||
TApiKeysInsert,
|
TApiKeysInsert,
|
||||||
TApiKeysUpdate,
|
TApiKeysUpdate,
|
||||||
|
TAppConnections,
|
||||||
|
TAppConnectionsInsert,
|
||||||
|
TAppConnectionsUpdate,
|
||||||
TAuditLogs,
|
TAuditLogs,
|
||||||
TAuditLogsInsert,
|
TAuditLogsInsert,
|
||||||
TAuditLogStreams,
|
TAuditLogStreams,
|
||||||
@@ -65,6 +68,9 @@ import {
|
|||||||
TDynamicSecrets,
|
TDynamicSecrets,
|
||||||
TDynamicSecretsInsert,
|
TDynamicSecretsInsert,
|
||||||
TDynamicSecretsUpdate,
|
TDynamicSecretsUpdate,
|
||||||
|
TExternalGroupOrgRoleMappings,
|
||||||
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
|
TExternalGroupOrgRoleMappingsUpdate,
|
||||||
TExternalKms,
|
TExternalKms,
|
||||||
TExternalKmsInsert,
|
TExternalKmsInsert,
|
||||||
TExternalKmsUpdate,
|
TExternalKmsUpdate,
|
||||||
@@ -226,6 +232,9 @@ import {
|
|||||||
TProjectSplitBackfillIds,
|
TProjectSplitBackfillIds,
|
||||||
TProjectSplitBackfillIdsInsert,
|
TProjectSplitBackfillIdsInsert,
|
||||||
TProjectSplitBackfillIdsUpdate,
|
TProjectSplitBackfillIdsUpdate,
|
||||||
|
TProjectSshConfigs,
|
||||||
|
TProjectSshConfigsInsert,
|
||||||
|
TProjectSshConfigsUpdate,
|
||||||
TProjectsUpdate,
|
TProjectsUpdate,
|
||||||
TProjectTemplates,
|
TProjectTemplates,
|
||||||
TProjectTemplatesInsert,
|
TProjectTemplatesInsert,
|
||||||
@@ -299,6 +308,12 @@ import {
|
|||||||
TSecretRotations,
|
TSecretRotations,
|
||||||
TSecretRotationsInsert,
|
TSecretRotationsInsert,
|
||||||
TSecretRotationsUpdate,
|
TSecretRotationsUpdate,
|
||||||
|
TSecretRotationsV2,
|
||||||
|
TSecretRotationsV2Insert,
|
||||||
|
TSecretRotationsV2Update,
|
||||||
|
TSecretRotationV2SecretMappings,
|
||||||
|
TSecretRotationV2SecretMappingsInsert,
|
||||||
|
TSecretRotationV2SecretMappingsUpdate,
|
||||||
TSecrets,
|
TSecrets,
|
||||||
TSecretScanningGitRisks,
|
TSecretScanningGitRisks,
|
||||||
TSecretScanningGitRisksInsert,
|
TSecretScanningGitRisksInsert,
|
||||||
@@ -320,15 +335,27 @@ import {
|
|||||||
TSecretSnapshotsInsert,
|
TSecretSnapshotsInsert,
|
||||||
TSecretSnapshotsUpdate,
|
TSecretSnapshotsUpdate,
|
||||||
TSecretsUpdate,
|
TSecretsUpdate,
|
||||||
|
TSecretsV2,
|
||||||
|
TSecretsV2Insert,
|
||||||
|
TSecretsV2Update,
|
||||||
|
TSecretSyncs,
|
||||||
|
TSecretSyncsInsert,
|
||||||
|
TSecretSyncsUpdate,
|
||||||
TSecretTagJunction,
|
TSecretTagJunction,
|
||||||
TSecretTagJunctionInsert,
|
TSecretTagJunctionInsert,
|
||||||
TSecretTagJunctionUpdate,
|
TSecretTagJunctionUpdate,
|
||||||
TSecretTags,
|
TSecretTags,
|
||||||
TSecretTagsInsert,
|
TSecretTagsInsert,
|
||||||
TSecretTagsUpdate,
|
TSecretTagsUpdate,
|
||||||
|
TSecretV2TagJunction,
|
||||||
|
TSecretV2TagJunctionInsert,
|
||||||
|
TSecretV2TagJunctionUpdate,
|
||||||
TSecretVersions,
|
TSecretVersions,
|
||||||
TSecretVersionsInsert,
|
TSecretVersionsInsert,
|
||||||
TSecretVersionsUpdate,
|
TSecretVersionsUpdate,
|
||||||
|
TSecretVersionsV2,
|
||||||
|
TSecretVersionsV2Insert,
|
||||||
|
TSecretVersionsV2Update,
|
||||||
TSecretVersionTagJunction,
|
TSecretVersionTagJunction,
|
||||||
TSecretVersionTagJunctionInsert,
|
TSecretVersionTagJunctionInsert,
|
||||||
TSecretVersionTagJunctionUpdate,
|
TSecretVersionTagJunctionUpdate,
|
||||||
@@ -356,6 +383,15 @@ import {
|
|||||||
TSshCertificateTemplates,
|
TSshCertificateTemplates,
|
||||||
TSshCertificateTemplatesInsert,
|
TSshCertificateTemplatesInsert,
|
||||||
TSshCertificateTemplatesUpdate,
|
TSshCertificateTemplatesUpdate,
|
||||||
|
TSshHostLoginUserMappings,
|
||||||
|
TSshHostLoginUserMappingsInsert,
|
||||||
|
TSshHostLoginUserMappingsUpdate,
|
||||||
|
TSshHostLoginUsers,
|
||||||
|
TSshHostLoginUsersInsert,
|
||||||
|
TSshHostLoginUsersUpdate,
|
||||||
|
TSshHosts,
|
||||||
|
TSshHostsInsert,
|
||||||
|
TSshHostsUpdate,
|
||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
@@ -387,24 +423,6 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections";
|
|
||||||
import {
|
|
||||||
TExternalGroupOrgRoleMappings,
|
|
||||||
TExternalGroupOrgRoleMappingsInsert,
|
|
||||||
TExternalGroupOrgRoleMappingsUpdate
|
|
||||||
} from "@app/db/schemas/external-group-org-role-mappings";
|
|
||||||
import { TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate } from "@app/db/schemas/secret-syncs";
|
|
||||||
import {
|
|
||||||
TSecretV2TagJunction,
|
|
||||||
TSecretV2TagJunctionInsert,
|
|
||||||
TSecretV2TagJunctionUpdate
|
|
||||||
} from "@app/db/schemas/secret-v2-tag-junction";
|
|
||||||
import {
|
|
||||||
TSecretVersionsV2,
|
|
||||||
TSecretVersionsV2Insert,
|
|
||||||
TSecretVersionsV2Update
|
|
||||||
} from "@app/db/schemas/secret-versions-v2";
|
|
||||||
import { TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas/secrets-v2";
|
|
||||||
|
|
||||||
declare module "knex" {
|
declare module "knex" {
|
||||||
namespace Knex {
|
namespace Knex {
|
||||||
@@ -419,6 +437,7 @@ declare module "knex/types/tables" {
|
|||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.SshHost]: KnexOriginal.CompositeTableType<TSshHosts, TSshHostsInsert, TSshHostsUpdate>;
|
||||||
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
TSshCertificateAuthorities,
|
TSshCertificateAuthorities,
|
||||||
TSshCertificateAuthoritiesInsert,
|
TSshCertificateAuthoritiesInsert,
|
||||||
@@ -444,6 +463,16 @@ declare module "knex/types/tables" {
|
|||||||
TSshCertificateBodiesInsert,
|
TSshCertificateBodiesInsert,
|
||||||
TSshCertificateBodiesUpdate
|
TSshCertificateBodiesUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.SshHostLoginUser]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshHostLoginUsers,
|
||||||
|
TSshHostLoginUsersInsert,
|
||||||
|
TSshHostLoginUsersUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshHostLoginUserMapping]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshHostLoginUserMappings,
|
||||||
|
TSshHostLoginUserMappingsInsert,
|
||||||
|
TSshHostLoginUserMappingsUpdate
|
||||||
|
>;
|
||||||
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateAuthorities,
|
TCertificateAuthorities,
|
||||||
TCertificateAuthoritiesInsert,
|
TCertificateAuthoritiesInsert,
|
||||||
@@ -548,6 +577,11 @@ declare module "knex/types/tables" {
|
|||||||
[TableName.SuperAdmin]: KnexOriginal.CompositeTableType<TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate>;
|
[TableName.SuperAdmin]: KnexOriginal.CompositeTableType<TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate>;
|
||||||
[TableName.ApiKey]: KnexOriginal.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
|
[TableName.ApiKey]: KnexOriginal.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
|
||||||
[TableName.Project]: KnexOriginal.CompositeTableType<TProjects, TProjectsInsert, TProjectsUpdate>;
|
[TableName.Project]: KnexOriginal.CompositeTableType<TProjects, TProjectsInsert, TProjectsUpdate>;
|
||||||
|
[TableName.ProjectSshConfig]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectSshConfigs,
|
||||||
|
TProjectSshConfigsInsert,
|
||||||
|
TProjectSshConfigsUpdate
|
||||||
|
>;
|
||||||
[TableName.ProjectMembership]: KnexOriginal.CompositeTableType<
|
[TableName.ProjectMembership]: KnexOriginal.CompositeTableType<
|
||||||
TProjectMemberships,
|
TProjectMemberships,
|
||||||
TProjectMembershipsInsert,
|
TProjectMembershipsInsert,
|
||||||
@@ -950,5 +984,15 @@ declare module "knex/types/tables" {
|
|||||||
TOrgGatewayConfigInsert,
|
TOrgGatewayConfigInsert,
|
||||||
TOrgGatewayConfigUpdate
|
TOrgGatewayConfigUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.SecretRotationV2]: KnexOriginal.CompositeTableType<
|
||||||
|
TSecretRotationsV2,
|
||||||
|
TSecretRotationsV2Insert,
|
||||||
|
TSecretRotationsV2Update
|
||||||
|
>;
|
||||||
|
[TableName.SecretRotationV2SecretMapping]: KnexOriginal.CompositeTableType<
|
||||||
|
TSecretRotationV2SecretMappings,
|
||||||
|
TSecretRotationV2SecretMappingsInsert,
|
||||||
|
TSecretRotationV2SecretMappingsUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManagedCredentials"))) {
|
||||||
|
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||||
|
t.boolean("isPlatformManagedCredentials").defaultTo(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManagedCredentials")) {
|
||||||
|
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||||
|
t.dropColumn("isPlatformManagedCredentials");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,58 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SecretRotationV2))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretRotationV2, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name", 32).notNullable();
|
||||||
|
t.string("description");
|
||||||
|
t.string("type").notNullable();
|
||||||
|
t.jsonb("parameters").notNullable();
|
||||||
|
t.jsonb("secretsMapping").notNullable();
|
||||||
|
t.binary("encryptedGeneratedCredentials").notNullable();
|
||||||
|
t.boolean("isAutoRotationEnabled").notNullable().defaultTo(true);
|
||||||
|
t.integer("activeIndex").notNullable().defaultTo(0);
|
||||||
|
t.uuid("folderId").notNullable();
|
||||||
|
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||||
|
t.uuid("connectionId").notNullable();
|
||||||
|
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.integer("rotationInterval").notNullable();
|
||||||
|
t.jsonb("rotateAtUtc").notNullable(); // { hours: number; minutes: number }
|
||||||
|
t.string("rotationStatus").notNullable();
|
||||||
|
t.datetime("lastRotationAttemptedAt").notNullable();
|
||||||
|
t.datetime("lastRotatedAt").notNullable();
|
||||||
|
t.binary("encryptedLastRotationMessage"); // we encrypt this because it may contain sensitive info (SQL errors showing credentials)
|
||||||
|
t.string("lastRotationJobId");
|
||||||
|
t.datetime("nextRotationAt");
|
||||||
|
t.boolean("isLastRotationManual").notNullable().defaultTo(true); // creation is considered a "manual" rotation
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretRotationV2);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretRotationV2, (t) => {
|
||||||
|
t.unique(["folderId", "name"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SecretRotationV2SecretMapping))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretRotationV2SecretMapping, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("secretId").notNullable();
|
||||||
|
// scott: this is deferred to block secret deletion but not prevent folder/environment/project deletion
|
||||||
|
// ie, if rotation is being deleted as well we permit it, otherwise throw
|
||||||
|
t.foreign("secretId").references("id").inTable(TableName.SecretV2).deferrable("deferred");
|
||||||
|
t.uuid("rotationId").notNullable();
|
||||||
|
t.foreign("rotationId").references("id").inTable(TableName.SecretRotationV2).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretRotationV2SecretMapping);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretRotationV2);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SecretRotationV2);
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.string("keySource");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backfilling the keySource to internal
|
||||||
|
await knex(TableName.SshCertificateAuthority).update({ keySource: "internal" });
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.string("keySource").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasColumn(TableName.SshCertificate, "sshCaId")) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
|
||||||
|
t.uuid("sshCaId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource")) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.dropColumn("keySource");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
93
backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts
Normal file
93
backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshHost))) {
|
||||||
|
await knex.schema.createTable(TableName.SshHost, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.string("hostname").notNullable();
|
||||||
|
t.string("userCertTtl").notNullable();
|
||||||
|
t.string("hostCertTtl").notNullable();
|
||||||
|
t.uuid("userSshCaId").notNullable();
|
||||||
|
t.foreign("userSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.uuid("hostSshCaId").notNullable();
|
||||||
|
t.foreign("hostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.unique(["projectId", "hostname"]);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshHostLoginUser))) {
|
||||||
|
await knex.schema.createTable(TableName.SshHostLoginUser, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshHostId").notNullable();
|
||||||
|
t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("CASCADE");
|
||||||
|
t.string("loginUser").notNullable(); // e.g. ubuntu, root, ec2-user, ...
|
||||||
|
t.unique(["sshHostId", "loginUser"]);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshHostLoginUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshHostLoginUserMapping))) {
|
||||||
|
await knex.schema.createTable(TableName.SshHostLoginUserMapping, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshHostLoginUserId").notNullable();
|
||||||
|
t.foreign("sshHostLoginUserId").references("id").inTable(TableName.SshHostLoginUser).onDelete("CASCADE");
|
||||||
|
t.uuid("userId").nullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.unique(["sshHostLoginUserId", "userId"]);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshHostLoginUserMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ProjectSshConfig))) {
|
||||||
|
// new table to store configuration for projects of type SSH (i.e. Infisical SSH)
|
||||||
|
await knex.schema.createTable(TableName.ProjectSshConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.uuid("defaultUserSshCaId");
|
||||||
|
t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.uuid("defaultHostSshCaId");
|
||||||
|
t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ProjectSshConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId");
|
||||||
|
if (!hasColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
|
||||||
|
t.uuid("sshHostId").nullable();
|
||||||
|
t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("SET NULL");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ProjectSshConfig);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectSshConfig);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshHostLoginUserMapping);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshHostLoginUserMapping);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshHostLoginUser);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshHostLoginUser);
|
||||||
|
|
||||||
|
const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId");
|
||||||
|
if (hasColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
|
||||||
|
t.dropColumn("sshHostId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshHost);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshHost);
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "note");
|
||||||
|
if (!hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||||
|
t.string("note").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "note");
|
||||||
|
if (hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||||
|
t.dropColumn("note");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -17,7 +17,8 @@ export const AccessApprovalRequestsSchema = z.object({
|
|||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
requestedByUserId: z.string().uuid()
|
requestedByUserId: z.string().uuid(),
|
||||||
|
note: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||||
|
@@ -19,7 +19,8 @@ export const AppConnectionsSchema = z.object({
|
|||||||
version: z.number().default(1),
|
version: z.number().default(1),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||||
|
@@ -3,6 +3,7 @@ export * from "./access-approval-policies-approvers";
|
|||||||
export * from "./access-approval-requests";
|
export * from "./access-approval-requests";
|
||||||
export * from "./access-approval-requests-reviewers";
|
export * from "./access-approval-requests-reviewers";
|
||||||
export * from "./api-keys";
|
export * from "./api-keys";
|
||||||
|
export * from "./app-connections";
|
||||||
export * from "./audit-log-streams";
|
export * from "./audit-log-streams";
|
||||||
export * from "./audit-logs";
|
export * from "./audit-logs";
|
||||||
export * from "./auth-token-sessions";
|
export * from "./auth-token-sessions";
|
||||||
@@ -19,6 +20,7 @@ export * from "./certificate-templates";
|
|||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
|
export * from "./external-group-org-role-mappings";
|
||||||
export * from "./external-kms";
|
export * from "./external-kms";
|
||||||
export * from "./gateways";
|
export * from "./gateways";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
@@ -73,6 +75,7 @@ export * from "./project-memberships";
|
|||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
export * from "./project-slack-configs";
|
export * from "./project-slack-configs";
|
||||||
export * from "./project-split-backfill-ids";
|
export * from "./project-split-backfill-ids";
|
||||||
|
export * from "./project-ssh-configs";
|
||||||
export * from "./project-templates";
|
export * from "./project-templates";
|
||||||
export * from "./project-user-additional-privilege";
|
export * from "./project-user-additional-privilege";
|
||||||
export * from "./project-user-membership-roles";
|
export * from "./project-user-membership-roles";
|
||||||
@@ -97,13 +100,16 @@ export * from "./secret-references";
|
|||||||
export * from "./secret-references-v2";
|
export * from "./secret-references-v2";
|
||||||
export * from "./secret-rotation-output-v2";
|
export * from "./secret-rotation-output-v2";
|
||||||
export * from "./secret-rotation-outputs";
|
export * from "./secret-rotation-outputs";
|
||||||
|
export * from "./secret-rotation-v2-secret-mappings";
|
||||||
export * from "./secret-rotations";
|
export * from "./secret-rotations";
|
||||||
|
export * from "./secret-rotations-v2";
|
||||||
export * from "./secret-scanning-git-risks";
|
export * from "./secret-scanning-git-risks";
|
||||||
export * from "./secret-sharing";
|
export * from "./secret-sharing";
|
||||||
export * from "./secret-snapshot-folders";
|
export * from "./secret-snapshot-folders";
|
||||||
export * from "./secret-snapshot-secrets";
|
export * from "./secret-snapshot-secrets";
|
||||||
export * from "./secret-snapshot-secrets-v2";
|
export * from "./secret-snapshot-secrets-v2";
|
||||||
export * from "./secret-snapshots";
|
export * from "./secret-snapshots";
|
||||||
|
export * from "./secret-syncs";
|
||||||
export * from "./secret-tag-junction";
|
export * from "./secret-tag-junction";
|
||||||
export * from "./secret-tags";
|
export * from "./secret-tags";
|
||||||
export * from "./secret-v2-tag-junction";
|
export * from "./secret-v2-tag-junction";
|
||||||
@@ -120,6 +126,9 @@ export * from "./ssh-certificate-authority-secrets";
|
|||||||
export * from "./ssh-certificate-bodies";
|
export * from "./ssh-certificate-bodies";
|
||||||
export * from "./ssh-certificate-templates";
|
export * from "./ssh-certificate-templates";
|
||||||
export * from "./ssh-certificates";
|
export * from "./ssh-certificates";
|
||||||
|
export * from "./ssh-host-login-user-mappings";
|
||||||
|
export * from "./ssh-host-login-users";
|
||||||
|
export * from "./ssh-hosts";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
export * from "./totp-configs";
|
export * from "./totp-configs";
|
||||||
export * from "./trusted-ips";
|
export * from "./trusted-ips";
|
||||||
|
@@ -2,6 +2,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
SshHost = "ssh_hosts",
|
||||||
|
SshHostLoginUser = "ssh_host_login_users",
|
||||||
|
SshHostLoginUserMapping = "ssh_host_login_user_mappings",
|
||||||
SshCertificateAuthority = "ssh_certificate_authorities",
|
SshCertificateAuthority = "ssh_certificate_authorities",
|
||||||
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
|
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
|
||||||
SshCertificateTemplate = "ssh_certificate_templates",
|
SshCertificateTemplate = "ssh_certificate_templates",
|
||||||
@@ -38,6 +41,7 @@ export enum TableName {
|
|||||||
SuperAdmin = "super_admin",
|
SuperAdmin = "super_admin",
|
||||||
RateLimit = "rate_limit",
|
RateLimit = "rate_limit",
|
||||||
ApiKey = "api_keys",
|
ApiKey = "api_keys",
|
||||||
|
ProjectSshConfig = "project_ssh_configs",
|
||||||
Project = "projects",
|
Project = "projects",
|
||||||
ProjectBot = "project_bots",
|
ProjectBot = "project_bots",
|
||||||
Environment = "project_environments",
|
Environment = "project_environments",
|
||||||
@@ -140,7 +144,9 @@ export enum TableName {
|
|||||||
KmipClient = "kmip_clients",
|
KmipClient = "kmip_clients",
|
||||||
KmipOrgConfig = "kmip_org_configs",
|
KmipOrgConfig = "kmip_org_configs",
|
||||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||||
KmipClientCertificates = "kmip_client_certificates"
|
KmipClientCertificates = "kmip_client_certificates",
|
||||||
|
SecretRotationV2 = "secret_rotations_v2",
|
||||||
|
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
@@ -23,10 +23,10 @@ export const OrganizationsSchema = z.object({
|
|||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false),
|
enforceMfa: z.boolean().default(false),
|
||||||
selectedMfaMethod: z.string().nullable().optional(),
|
selectedMfaMethod: z.string().nullable().optional(),
|
||||||
|
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
privilegeUpgradeInitiatedAt: z.date().nullable().optional()
|
||||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
21
backend/src/db/schemas/project-ssh-configs.ts
Normal file
21
backend/src/db/schemas/project-ssh-configs.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// 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 ProjectSshConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
projectId: z.string(),
|
||||||
|
defaultUserSshCaId: z.string().uuid().nullable().optional(),
|
||||||
|
defaultHostSshCaId: z.string().uuid().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectSshConfigs = z.infer<typeof ProjectSshConfigsSchema>;
|
||||||
|
export type TProjectSshConfigsInsert = Omit<z.input<typeof ProjectSshConfigsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TProjectSshConfigsUpdate = Partial<Omit<z.input<typeof ProjectSshConfigsSchema>, TImmutableDBKeys>>;
|
23
backend/src/db/schemas/secret-rotation-v2-secret-mappings.ts
Normal file
23
backend/src/db/schemas/secret-rotation-v2-secret-mappings.ts
Normal file
@@ -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 SecretRotationV2SecretMappingsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
secretId: z.string().uuid(),
|
||||||
|
rotationId: z.string().uuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretRotationV2SecretMappings = z.infer<typeof SecretRotationV2SecretMappingsSchema>;
|
||||||
|
export type TSecretRotationV2SecretMappingsInsert = Omit<
|
||||||
|
z.input<typeof SecretRotationV2SecretMappingsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TSecretRotationV2SecretMappingsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SecretRotationV2SecretMappingsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
39
backend/src/db/schemas/secret-rotations-v2.ts
Normal file
39
backend/src/db/schemas/secret-rotations-v2.ts
Normal file
@@ -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 SecretRotationsV2Schema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
type: z.string(),
|
||||||
|
parameters: z.unknown(),
|
||||||
|
secretsMapping: z.unknown(),
|
||||||
|
encryptedGeneratedCredentials: zodBuffer,
|
||||||
|
isAutoRotationEnabled: z.boolean().default(true),
|
||||||
|
activeIndex: z.number().default(0),
|
||||||
|
folderId: z.string().uuid(),
|
||||||
|
connectionId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
rotationInterval: z.number(),
|
||||||
|
rotateAtUtc: z.unknown(),
|
||||||
|
rotationStatus: z.string(),
|
||||||
|
lastRotationAttemptedAt: z.date(),
|
||||||
|
lastRotatedAt: z.date(),
|
||||||
|
encryptedLastRotationMessage: zodBuffer.nullable().optional(),
|
||||||
|
lastRotationJobId: z.string().nullable().optional(),
|
||||||
|
nextRotationAt: z.date().nullable().optional(),
|
||||||
|
isLastRotationManual: z.boolean().default(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretRotationsV2 = z.infer<typeof SecretRotationsV2Schema>;
|
||||||
|
export type TSecretRotationsV2Insert = Omit<z.input<typeof SecretRotationsV2Schema>, TImmutableDBKeys>;
|
||||||
|
export type TSecretRotationsV2Update = Partial<Omit<z.input<typeof SecretRotationsV2Schema>, TImmutableDBKeys>>;
|
@@ -14,7 +14,8 @@ export const SshCertificateAuthoritiesSchema = z.object({
|
|||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
keyAlgorithm: z.string()
|
keyAlgorithm: z.string(),
|
||||||
|
keySource: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
||||||
|
@@ -11,14 +11,15 @@ export const SshCertificatesSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
sshCaId: z.string().uuid(),
|
sshCaId: z.string().uuid().nullable().optional(),
|
||||||
sshCertificateTemplateId: z.string().uuid().nullable().optional(),
|
sshCertificateTemplateId: z.string().uuid().nullable().optional(),
|
||||||
serialNumber: z.string(),
|
serialNumber: z.string(),
|
||||||
certType: z.string(),
|
certType: z.string(),
|
||||||
principals: z.string().array(),
|
principals: z.string().array(),
|
||||||
keyId: z.string(),
|
keyId: z.string(),
|
||||||
notBefore: z.date(),
|
notBefore: z.date(),
|
||||||
notAfter: z.date()
|
notAfter: z.date(),
|
||||||
|
sshHostId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
|
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
|
||||||
|
22
backend/src/db/schemas/ssh-host-login-user-mappings.ts
Normal file
22
backend/src/db/schemas/ssh-host-login-user-mappings.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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 SshHostLoginUserMappingsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshHostLoginUserId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshHostLoginUserMappings = z.infer<typeof SshHostLoginUserMappingsSchema>;
|
||||||
|
export type TSshHostLoginUserMappingsInsert = Omit<z.input<typeof SshHostLoginUserMappingsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshHostLoginUserMappingsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshHostLoginUserMappingsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
20
backend/src/db/schemas/ssh-host-login-users.ts
Normal file
20
backend/src/db/schemas/ssh-host-login-users.ts
Normal file
@@ -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 SshHostLoginUsersSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshHostId: z.string().uuid(),
|
||||||
|
loginUser: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshHostLoginUsers = z.infer<typeof SshHostLoginUsersSchema>;
|
||||||
|
export type TSshHostLoginUsersInsert = Omit<z.input<typeof SshHostLoginUsersSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshHostLoginUsersUpdate = Partial<Omit<z.input<typeof SshHostLoginUsersSchema>, TImmutableDBKeys>>;
|
24
backend/src/db/schemas/ssh-hosts.ts
Normal file
24
backend/src/db/schemas/ssh-hosts.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// 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 SshHostsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
projectId: z.string(),
|
||||||
|
hostname: z.string(),
|
||||||
|
userCertTtl: z.string(),
|
||||||
|
hostCertTtl: z.string(),
|
||||||
|
userSshCaId: z.string().uuid(),
|
||||||
|
hostSshCaId: z.string().uuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshHosts = z.infer<typeof SshHostsSchema>;
|
||||||
|
export type TSshHostsInsert = Omit<z.input<typeof SshHostsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshHostsUpdate = Partial<Omit<z.input<typeof SshHostsSchema>, TImmutableDBKeys>>;
|
@@ -22,7 +22,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
permissions: z.any().array(),
|
permissions: z.any().array(),
|
||||||
isTemporary: z.boolean(),
|
isTemporary: z.boolean(),
|
||||||
temporaryRange: z.string().optional()
|
temporaryRange: z.string().optional(),
|
||||||
|
note: z.string().max(255).optional()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectSlug: z.string().trim()
|
projectSlug: z.string().trim()
|
||||||
@@ -43,7 +44,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectSlug: req.query.projectSlug,
|
projectSlug: req.query.projectSlug,
|
||||||
temporaryRange: req.body.temporaryRange,
|
temporaryRange: req.body.temporaryRange,
|
||||||
isTemporary: req.body.isTemporary
|
isTemporary: req.body.isTemporary,
|
||||||
|
note: req.body.note
|
||||||
});
|
});
|
||||||
return { approval: request };
|
return { approval: request };
|
||||||
}
|
}
|
||||||
|
@@ -32,6 +32,7 @@ import { registerSnapshotRouter } from "./snapshot-router";
|
|||||||
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
||||||
import { registerSshCertRouter } from "./ssh-certificate-router";
|
import { registerSshCertRouter } from "./ssh-certificate-router";
|
||||||
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
||||||
|
import { registerSshHostRouter } from "./ssh-host-router";
|
||||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
|
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
|
||||||
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
||||||
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
|
await sshRouter.register(registerSshHostRouter, { prefix: "/hosts" });
|
||||||
},
|
},
|
||||||
{ prefix: "/ssh" }
|
{ prefix: "/ssh" }
|
||||||
);
|
);
|
||||||
|
@@ -277,8 +277,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
commits: secretRawSchema
|
commits: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
|
||||||
.extend({
|
.extend({
|
||||||
|
secretValue: z.string().optional(),
|
||||||
|
isRotatedSecret: z.boolean().optional(),
|
||||||
op: z.string(),
|
op: z.string(),
|
||||||
tags: SanitizedTagSchema.array().optional(),
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.nullish(),
|
secretMetadata: ResourceMetadataSchema.nullish(),
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -40,16 +41,10 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async () => {
|
||||||
const secretRotation = await server.services.secretRotation.createRotation({
|
throw new BadRequestError({
|
||||||
actor: req.permission.type,
|
message: `This version of Secret Rotations has been deprecated. Please see docs for new version.`
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
...req.body,
|
|
||||||
projectId: req.body.workspaceId
|
|
||||||
});
|
});
|
||||||
return { secretRotation };
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -33,7 +33,8 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
.extend({
|
.extend({
|
||||||
secretValueHidden: z.boolean(),
|
secretValueHidden: z.boolean(),
|
||||||
secretId: z.string(),
|
secretId: z.string(),
|
||||||
tags: SanitizedTagSchema.array()
|
tags: SanitizedTagSchema.array(),
|
||||||
|
isRotatedSecret: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { normalizeSshPrivateKey } from "@app/ee/services/ssh/ssh-certificate-authority-fns";
|
||||||
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
import { SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaKeySource, SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
|
||||||
|
|
||||||
export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -20,14 +21,34 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
description: "Create SSH CA",
|
description: "Create SSH CA",
|
||||||
body: z.object({
|
body: z
|
||||||
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
.object({
|
||||||
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
||||||
keyAlgorithm: z
|
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||||
.nativeEnum(CertKeyAlgorithm)
|
keyAlgorithm: z
|
||||||
.default(CertKeyAlgorithm.RSA_2048)
|
.nativeEnum(SshCertKeyAlgorithm)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
.default(SshCertKeyAlgorithm.ED25519)
|
||||||
}),
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm),
|
||||||
|
publicKey: z.string().trim().optional().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.publicKey),
|
||||||
|
privateKey: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => (val ? normalizeSshPrivateKey(val) : undefined))
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.privateKey),
|
||||||
|
keySource: z
|
||||||
|
.nativeEnum(SshCaKeySource)
|
||||||
|
.default(SshCaKeySource.INTERNAL)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keySource)
|
||||||
|
})
|
||||||
|
.refine((data) => data.keySource === SshCaKeySource.INTERNAL || (!!data.publicKey && !!data.privateKey), {
|
||||||
|
message: "publicKey and privateKey are required when keySource is external",
|
||||||
|
path: ["publicKey"]
|
||||||
|
})
|
||||||
|
.refine((data) => data.keySource === SshCaKeySource.EXTERNAL || !!data.keyAlgorithm, {
|
||||||
|
message: "keyAlgorithm is required when keySource is internal",
|
||||||
|
path: ["keyAlgorithm"]
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
ca: sanitizedSshCa.extend({
|
ca: sanitizedSshCa.extend({
|
||||||
|
@@ -2,13 +2,13 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -108,8 +108,8 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(1)
|
.min(1)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId),
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId),
|
||||||
keyAlgorithm: z
|
keyAlgorithm: z
|
||||||
.nativeEnum(CertKeyAlgorithm)
|
.nativeEnum(SshCertKeyAlgorithm)
|
||||||
.default(CertKeyAlgorithm.RSA_2048)
|
.default(SshCertKeyAlgorithm.ED25519)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
|
||||||
certType: z
|
certType: z
|
||||||
.nativeEnum(SshCertType)
|
.nativeEnum(SshCertType)
|
||||||
@@ -133,7 +133,7 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey),
|
privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey),
|
||||||
publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey),
|
publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey),
|
||||||
keyAlgorithm: z
|
keyAlgorithm: z
|
||||||
.nativeEnum(CertKeyAlgorithm)
|
.nativeEnum(SshCertKeyAlgorithm)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -92,8 +92,8 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
|
|||||||
allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates),
|
allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates),
|
||||||
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
||||||
})
|
})
|
||||||
.refine((data) => ms(data.maxTTL) > ms(data.ttl), {
|
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
|
||||||
message: "Max TLL must be greater than TTL",
|
message: "Max TLL must be greater than or equal to TTL",
|
||||||
path: ["maxTTL"]
|
path: ["maxTTL"]
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
444
backend/src/ee/routes/v1/ssh-host-router.ts
Normal file
444
backend/src/ee/routes/v1/ssh-host-router.ts
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
|
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||||
|
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
|
||||||
|
import { SSH_HOSTS } from "@app/lib/api-docs";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
|
export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.array(
|
||||||
|
sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const hosts = await server.services.sshHost.listSshHosts({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshHostId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const host = await server.services.sshHost.getSshHost({
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: host.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_HOST,
|
||||||
|
metadata: {
|
||||||
|
sshHostId: host.id,
|
||||||
|
hostname: host.hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Add an SSH Host",
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().describe(SSH_HOSTS.CREATE.projectId),
|
||||||
|
hostname: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine((v) => isValidHostname(v), {
|
||||||
|
message: "Hostname must be a valid hostname"
|
||||||
|
})
|
||||||
|
.describe(SSH_HOSTS.CREATE.hostname),
|
||||||
|
userCertTtl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.default("8h")
|
||||||
|
.describe(SSH_HOSTS.CREATE.userCertTtl),
|
||||||
|
hostCertTtl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.default("1y")
|
||||||
|
.describe(SSH_HOSTS.CREATE.hostCertTtl),
|
||||||
|
loginMappings: z.array(loginMappingSchema).default([]).describe(SSH_HOSTS.CREATE.loginMappings),
|
||||||
|
userSshCaId: z.string().describe(SSH_HOSTS.CREATE.userSshCaId).optional(),
|
||||||
|
hostSshCaId: z.string().describe(SSH_HOSTS.CREATE.hostSshCaId).optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const host = await server.services.sshHost.createSshHost({
|
||||||
|
...req.body,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: host.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SSH_HOST,
|
||||||
|
metadata: {
|
||||||
|
sshHostId: host.id,
|
||||||
|
hostname: host.hostname,
|
||||||
|
userCertTtl: host.userCertTtl,
|
||||||
|
hostCertTtl: host.hostCertTtl,
|
||||||
|
loginMappings: host.loginMappings,
|
||||||
|
userSshCaId: host.userSshCaId,
|
||||||
|
hostSshCaId: host.hostSshCaId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:sshHostId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update SSH Host",
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
hostname: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine((v) => isValidHostname(v), {
|
||||||
|
message: "Hostname must be a valid hostname"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_HOSTS.UPDATE.hostname),
|
||||||
|
userCertTtl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_HOSTS.UPDATE.userCertTtl),
|
||||||
|
hostCertTtl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_HOSTS.UPDATE.hostCertTtl),
|
||||||
|
loginMappings: z.array(loginMappingSchema).optional().describe(SSH_HOSTS.UPDATE.loginMappings)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const host = await server.services.sshHost.updateSshHost({
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
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,
|
||||||
|
projectId: host.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SSH_HOST,
|
||||||
|
metadata: {
|
||||||
|
sshHostId: host.id,
|
||||||
|
hostname: host.hostname,
|
||||||
|
userCertTtl: host.userCertTtl,
|
||||||
|
hostCertTtl: host.hostCertTtl,
|
||||||
|
loginMappings: host.loginMappings,
|
||||||
|
userSshCaId: host.userSshCaId,
|
||||||
|
hostSshCaId: host.hostSshCaId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:sshHostId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const host = await server.services.sshHost.deleteSshHost({
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: host.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SSH_HOST,
|
||||||
|
metadata: {
|
||||||
|
sshHostId: host.id,
|
||||||
|
hostname: host.hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:sshHostId/issue-user-cert",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
description: "Issue SSH certificate for user",
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
loginUser: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.loginUser)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.serialNumber),
|
||||||
|
signedKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.signedKey),
|
||||||
|
privateKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.privateKey),
|
||||||
|
publicKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.publicKey),
|
||||||
|
keyAlgorithm: z.nativeEnum(SshCertKeyAlgorithm).describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } =
|
||||||
|
await server.services.sshHost.issueSshHostUserCert({
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
loginUser: req.body.loginUser,
|
||||||
|
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.ISSUE_SSH_HOST_USER_CERT,
|
||||||
|
metadata: {
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
hostname: host.hostname,
|
||||||
|
loginUser: req.body.loginUser,
|
||||||
|
principals,
|
||||||
|
ttl: host.userCertTtl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.IssueSshHostUserCert,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
hostname: host.hostname,
|
||||||
|
principals,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
keyAlgorithm
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:sshHostId/issue-host-cert",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Issue SSH certificate for host",
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
publicKey: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.publicKey)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.serialNumber),
|
||||||
|
signedKey: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.signedKey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { host, principals, serialNumber, signedPublicKey } = await server.services.sshHost.issueSshHostHostCert({
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
publicKey: req.body.publicKey,
|
||||||
|
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.ISSUE_SSH_HOST_HOST_CERT,
|
||||||
|
metadata: {
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
hostname: host.hostname,
|
||||||
|
principals,
|
||||||
|
serialNumber,
|
||||||
|
ttl: host.hostCertTtl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.IssueSshHostHostCert,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
hostname: host.hostname,
|
||||||
|
principals,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshHostId/user-ca-public-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: publicSshCaLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get public key of the user SSH CA linked to the host",
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.publicKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const publicKey = await server.services.sshHost.getSshHostUserCaPk(req.params.sshHostId);
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshHostId/host-ca-public-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: publicSshCaLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get public key of the host SSH CA linked to the host",
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.publicKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const publicKey = await server.services.sshHost.getSshHostHostCaPk(req.params.sshHostId);
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
registerSecretRotationV2Router,
|
||||||
|
SECRET_ROTATION_REGISTER_ROUTER_MAP
|
||||||
|
} from "@app/ee/routes/v2/secret-rotation-v2-routers";
|
||||||
|
|
||||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
import { registerProjectRoleRouter } from "./project-role-router";
|
||||||
|
|
||||||
@@ -13,4 +18,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerIdentityProjectAdditionalPrivilegeRouter, {
|
await server.register(registerIdentityProjectAdditionalPrivilegeRouter, {
|
||||||
prefix: "/identity-project-additional-privilege"
|
prefix: "/identity-project-additional-privilege"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (secretRotationV2Router) => {
|
||||||
|
// register generic secret rotation endpoints
|
||||||
|
await secretRotationV2Router.register(registerSecretRotationV2Router);
|
||||||
|
|
||||||
|
// register service specific secret rotation endpoints (secret-rotations/postgres-credentials, etc.)
|
||||||
|
for await (const [type, router] of Object.entries(SECRET_ROTATION_REGISTER_ROUTER_MAP)) {
|
||||||
|
await secretRotationV2Router.register(router, { prefix: `/${type}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prefix: "/secret-rotations" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
14
backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
Normal file
14
backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
|
||||||
|
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||||
|
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||||
|
|
||||||
|
export * from "./secret-rotation-v2-router";
|
||||||
|
|
||||||
|
export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||||
|
SecretRotation,
|
||||||
|
(server: FastifyZodProvider) => Promise<void>
|
||||||
|
> = {
|
||||||
|
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||||
|
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter
|
||||||
|
};
|
@@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
CreateMsSqlCredentialsRotationSchema,
|
||||||
|
MsSqlCredentialsRotationSchema,
|
||||||
|
UpdateMsSqlCredentialsRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
|
||||||
|
|
||||||
|
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||||
|
|
||||||
|
export const registerMsSqlCredentialsRotationRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSecretRotationEndpoints({
|
||||||
|
type: SecretRotation.MsSqlCredentials,
|
||||||
|
server,
|
||||||
|
responseSchema: MsSqlCredentialsRotationSchema,
|
||||||
|
createSchema: CreateMsSqlCredentialsRotationSchema,
|
||||||
|
updateSchema: UpdateMsSqlCredentialsRotationSchema,
|
||||||
|
generatedCredentialsSchema: SqlCredentialsRotationGeneratedCredentialsSchema
|
||||||
|
});
|
@@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
CreatePostgresCredentialsRotationSchema,
|
||||||
|
PostgresCredentialsRotationSchema,
|
||||||
|
UpdatePostgresCredentialsRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
|
||||||
|
|
||||||
|
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||||
|
|
||||||
|
export const registerPostgresCredentialsRotationRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSecretRotationEndpoints({
|
||||||
|
type: SecretRotation.PostgresCredentials,
|
||||||
|
server,
|
||||||
|
responseSchema: PostgresCredentialsRotationSchema,
|
||||||
|
createSchema: CreatePostgresCredentialsRotationSchema,
|
||||||
|
updateSchema: UpdatePostgresCredentialsRotationSchema,
|
||||||
|
generatedCredentialsSchema: SqlCredentialsRotationGeneratedCredentialsSchema
|
||||||
|
});
|
@@ -0,0 +1,429 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { SECRET_ROTATION_NAME_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||||
|
import {
|
||||||
|
TRotateAtUtc,
|
||||||
|
TSecretRotationV2,
|
||||||
|
TSecretRotationV2GeneratedCredentials,
|
||||||
|
TSecretRotationV2Input
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { SecretRotations } from "@app/lib/api-docs";
|
||||||
|
import { startsWithVowel } from "@app/lib/fn";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const registerSecretRotationEndpoints = <
|
||||||
|
T extends TSecretRotationV2,
|
||||||
|
I extends TSecretRotationV2Input,
|
||||||
|
C extends TSecretRotationV2GeneratedCredentials
|
||||||
|
>({
|
||||||
|
server,
|
||||||
|
type,
|
||||||
|
createSchema,
|
||||||
|
updateSchema,
|
||||||
|
responseSchema,
|
||||||
|
generatedCredentialsSchema
|
||||||
|
}: {
|
||||||
|
type: SecretRotation;
|
||||||
|
server: FastifyZodProvider;
|
||||||
|
createSchema: z.ZodType<{
|
||||||
|
name: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
projectId: string;
|
||||||
|
connectionId: string;
|
||||||
|
parameters: I["parameters"];
|
||||||
|
secretsMapping: I["secretsMapping"];
|
||||||
|
description?: string | null;
|
||||||
|
isAutoRotationEnabled?: boolean;
|
||||||
|
rotationInterval: number;
|
||||||
|
rotateAtUtc?: TRotateAtUtc;
|
||||||
|
}>;
|
||||||
|
updateSchema: z.ZodType<{
|
||||||
|
connectionId?: string;
|
||||||
|
name?: string;
|
||||||
|
environment?: string;
|
||||||
|
secretPath?: string;
|
||||||
|
parameters?: I["parameters"];
|
||||||
|
secretsMapping?: I["secretsMapping"];
|
||||||
|
description?: string | null;
|
||||||
|
isAutoRotationEnabled?: boolean;
|
||||||
|
rotationInterval?: number;
|
||||||
|
rotateAtUtc?: TRotateAtUtc;
|
||||||
|
}>;
|
||||||
|
responseSchema: z.ZodTypeAny;
|
||||||
|
generatedCredentialsSchema: z.ZodTypeAny;
|
||||||
|
}) => {
|
||||||
|
const rotationType = SECRET_ROTATION_NAME_MAP[type];
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `List the ${rotationType} Rotations for the specified project.`,
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST(type).projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretRotations: responseSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
query: { projectId }
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const secretRotations = (await server.services.secretRotationV2.listSecretRotationsByProjectId(
|
||||||
|
{ projectId, type },
|
||||||
|
req.permission
|
||||||
|
)) as T[];
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_ROTATIONS,
|
||||||
|
metadata: {
|
||||||
|
type,
|
||||||
|
count: secretRotations.length,
|
||||||
|
rotationIds: secretRotations.map((rotation) => rotation.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRotations };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:rotationId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${rotationType} Rotation by ID.`,
|
||||||
|
params: z.object({
|
||||||
|
rotationId: z.string().uuid().describe(SecretRotations.GET_BY_ID(type).rotationId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretRotation: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { rotationId } = req.params;
|
||||||
|
|
||||||
|
const secretRotation = (await server.services.secretRotationV2.findSecretRotationById(
|
||||||
|
{ rotationId, type },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: secretRotation.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_ROTATION,
|
||||||
|
metadata: {
|
||||||
|
rotationId,
|
||||||
|
type,
|
||||||
|
secretPath: secretRotation.folder.path,
|
||||||
|
environment: secretRotation.environment.slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRotation };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/rotation-name/:rotationName`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${rotationType} Rotation by name, secret path, environment and project ID.`,
|
||||||
|
params: z.object({
|
||||||
|
rotationName: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Rotation name required")
|
||||||
|
.describe(SecretRotations.GET_BY_NAME(type).rotationName)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Project ID required")
|
||||||
|
.describe(SecretRotations.GET_BY_NAME(type).projectId),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Secret path required")
|
||||||
|
.describe(SecretRotations.GET_BY_NAME(type).secretPath),
|
||||||
|
environment: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Environment required")
|
||||||
|
.describe(SecretRotations.GET_BY_NAME(type).environment)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretRotation: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { rotationName } = req.params;
|
||||||
|
const { projectId, secretPath, environment } = req.query;
|
||||||
|
|
||||||
|
const secretRotation = (await server.services.secretRotationV2.findSecretRotationByName(
|
||||||
|
{ rotationName, projectId, type, secretPath, environment },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_ROTATION,
|
||||||
|
metadata: {
|
||||||
|
rotationId: secretRotation.id,
|
||||||
|
type,
|
||||||
|
secretPath,
|
||||||
|
environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRotation };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Create ${
|
||||||
|
startsWithVowel(rotationType) ? "an" : "a"
|
||||||
|
} ${rotationType} Rotation for the specified project.`,
|
||||||
|
body: createSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretRotation: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRotation = (await server.services.secretRotationV2.createSecretRotation(
|
||||||
|
{ ...req.body, type },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: secretRotation.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SECRET_ROTATION,
|
||||||
|
metadata: {
|
||||||
|
rotationId: secretRotation.id,
|
||||||
|
type,
|
||||||
|
...req.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRotation };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:rotationId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Update the specified ${rotationType} Rotation.`,
|
||||||
|
params: z.object({
|
||||||
|
rotationId: z.string().uuid().describe(SecretRotations.UPDATE(type).rotationId)
|
||||||
|
}),
|
||||||
|
body: updateSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretRotation: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { rotationId } = req.params;
|
||||||
|
|
||||||
|
const secretRotation = (await server.services.secretRotationV2.updateSecretRotation(
|
||||||
|
{ ...req.body, rotationId, type },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: secretRotation.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SECRET_ROTATION,
|
||||||
|
metadata: {
|
||||||
|
rotationId,
|
||||||
|
type,
|
||||||
|
...req.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRotation };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/:rotationId`,
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Delete the specified ${rotationType} Rotation.`,
|
||||||
|
params: z.object({
|
||||||
|
rotationId: z.string().uuid().describe(SecretRotations.DELETE(type).rotationId)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
deleteSecrets: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
.describe(SecretRotations.DELETE(type).deleteSecrets),
|
||||||
|
revokeGeneratedCredentials: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
.describe(SecretRotations.DELETE(type).revokeGeneratedCredentials)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretRotation: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { rotationId } = req.params;
|
||||||
|
const { deleteSecrets, revokeGeneratedCredentials } = req.query;
|
||||||
|
|
||||||
|
const secretRotation = (await server.services.secretRotationV2.deleteSecretRotation(
|
||||||
|
{ type, rotationId, deleteSecrets, revokeGeneratedCredentials },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: secretRotation.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SECRET_ROTATION,
|
||||||
|
metadata: {
|
||||||
|
type,
|
||||||
|
rotationId,
|
||||||
|
deleteSecrets,
|
||||||
|
revokeGeneratedCredentials
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRotation };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:rotationId/generated-credentials",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the generated credentials for the specified ${rotationType} Rotation.`,
|
||||||
|
params: z.object({
|
||||||
|
rotationId: z.string().uuid().describe(SecretRotations.GET_GENERATED_CREDENTIALS_BY_ID(type).rotationId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
generatedCredentials: generatedCredentialsSchema,
|
||||||
|
activeIndex: z.number(),
|
||||||
|
rotationId: z.string().uuid(),
|
||||||
|
type: z.literal(type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { rotationId } = req.params;
|
||||||
|
|
||||||
|
const {
|
||||||
|
generatedCredentials,
|
||||||
|
secretRotation: { activeIndex, projectId, folder, environment }
|
||||||
|
} = await server.services.secretRotationV2.findSecretRotationGeneratedCredentialsById(
|
||||||
|
{
|
||||||
|
rotationId,
|
||||||
|
type
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS,
|
||||||
|
metadata: {
|
||||||
|
type,
|
||||||
|
rotationId,
|
||||||
|
secretPath: folder.path,
|
||||||
|
environment: environment.slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { generatedCredentials: generatedCredentials as C, activeIndex, rotationId, type };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:rotationId/rotate-secrets",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Rotate the generated credentials for the specified ${rotationType} Rotation.`,
|
||||||
|
params: z.object({
|
||||||
|
rotationId: z.string().uuid().describe(SecretRotations.ROTATE(type).rotationId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretRotation: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { rotationId } = req.params;
|
||||||
|
|
||||||
|
const secretRotation = (await server.services.secretRotationV2.rotateSecretRotation(
|
||||||
|
{
|
||||||
|
rotationId,
|
||||||
|
type,
|
||||||
|
auditLogInfo: req.auditLogInfo
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
return { secretRotation };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -0,0 +1,81 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
|
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
|
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||||
|
import { SecretRotations } from "@app/lib/api-docs";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||||
|
PostgresCredentialsRotationListItemSchema,
|
||||||
|
MsSqlCredentialsRotationListItemSchema
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/options",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List the available Secret Rotation Options.",
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRotationOptions: SecretRotationV2OptionsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: () => {
|
||||||
|
const secretRotationOptions = server.services.secretRotationV2.listSecretRotationOptions();
|
||||||
|
return { secretRotationOptions };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List all the Secret Rotations for the specified project.",
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST().projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretRotations: SecretRotationV2Schema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
query: { projectId },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const secretRotations = await server.services.secretRotationV2.listSecretRotationsByProjectId(
|
||||||
|
{ projectId },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_ROTATIONS,
|
||||||
|
metadata: {
|
||||||
|
rotationIds: secretRotations.map((sync) => sync.id),
|
||||||
|
count: secretRotations.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRotations };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -94,7 +94,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectSlug
|
projectSlug,
|
||||||
|
note
|
||||||
}: TCreateAccessApprovalRequestDTO) => {
|
}: TCreateAccessApprovalRequestDTO) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
@@ -209,7 +210,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
requestedByUserId: actorId,
|
requestedByUserId: actorId,
|
||||||
temporaryRange: temporaryRange || null,
|
temporaryRange: temporaryRange || null,
|
||||||
permissions: JSON.stringify(requestedPermissions),
|
permissions: JSON.stringify(requestedPermissions),
|
||||||
isTemporary
|
isTemporary,
|
||||||
|
note: note || null
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -232,7 +234,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
environment: envSlug,
|
environment: envSlug,
|
||||||
permissions: accessTypes,
|
permissions: accessTypes,
|
||||||
approvalUrl
|
approvalUrl,
|
||||||
|
note
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -252,7 +255,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
environment: envSlug,
|
environment: envSlug,
|
||||||
permissions: accessTypes,
|
permissions: accessTypes,
|
||||||
approvalUrl
|
approvalUrl,
|
||||||
|
note
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.AccessApprovalRequest
|
template: SmtpTemplates.AccessApprovalRequest
|
||||||
});
|
});
|
||||||
|
@@ -24,6 +24,7 @@ export type TCreateAccessApprovalRequestDTO = {
|
|||||||
permissions: unknown;
|
permissions: unknown;
|
||||||
isTemporary: boolean;
|
isTemporary: boolean;
|
||||||
temporaryRange?: string;
|
temporaryRange?: string;
|
||||||
|
note?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TListApprovalRequestsDTO = {
|
export type TListApprovalRequestsDTO = {
|
||||||
|
@@ -2,7 +2,15 @@ import {
|
|||||||
TCreateProjectTemplateDTO,
|
TCreateProjectTemplateDTO,
|
||||||
TUpdateProjectTemplateDTO
|
TUpdateProjectTemplateDTO
|
||||||
} from "@app/ee/services/project-template/project-template-types";
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
|
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import {
|
||||||
|
TCreateSecretRotationV2DTO,
|
||||||
|
TDeleteSecretRotationV2DTO,
|
||||||
|
TSecretRotationV2Raw,
|
||||||
|
TUpdateSecretRotationV2DTO
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
@@ -56,6 +64,8 @@ export type TCreateAuditLogDTO = {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
} & BaseAuthData;
|
} & BaseAuthData;
|
||||||
|
|
||||||
|
export type AuditLogInfo = Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
||||||
|
|
||||||
interface BaseAuthData {
|
interface BaseAuthData {
|
||||||
ipAddress?: string;
|
ipAddress?: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
@@ -180,6 +190,12 @@ export enum EventType {
|
|||||||
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||||
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||||
|
CREATE_SSH_HOST = "create-ssh-host",
|
||||||
|
UPDATE_SSH_HOST = "update-ssh-host",
|
||||||
|
DELETE_SSH_HOST = "delete-ssh-host",
|
||||||
|
GET_SSH_HOST = "get-ssh-host",
|
||||||
|
ISSUE_SSH_HOST_USER_CERT = "issue-ssh-host-user-cert",
|
||||||
|
ISSUE_SSH_HOST_HOST_CERT = "issue-ssh-host-host-cert",
|
||||||
CREATE_CA = "create-certificate-authority",
|
CREATE_CA = "create-certificate-authority",
|
||||||
GET_CA = "get-certificate-authority",
|
GET_CA = "get-certificate-authority",
|
||||||
UPDATE_CA = "update-certificate-authority",
|
UPDATE_CA = "update-certificate-authority",
|
||||||
@@ -287,6 +303,14 @@ export enum EventType {
|
|||||||
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||||
KMIP_OPERATION_REGISTER = "kmip-operation-register",
|
KMIP_OPERATION_REGISTER = "kmip-operation-register",
|
||||||
|
|
||||||
|
GET_SECRET_ROTATIONS = "get-secret-rotations",
|
||||||
|
GET_SECRET_ROTATION = "get-secret-rotation",
|
||||||
|
GET_SECRET_ROTATION_GENERATED_CREDENTIALS = "get-secret-rotation-generated-credentials",
|
||||||
|
CREATE_SECRET_ROTATION = "create-secret-rotation",
|
||||||
|
UPDATE_SECRET_ROTATION = "update-secret-rotation",
|
||||||
|
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
||||||
|
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||||
|
|
||||||
PROJECT_ACCESS_REQUEST = "project-access-request"
|
PROJECT_ACCESS_REQUEST = "project-access-request"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1360,7 +1384,7 @@ interface IssueSshCreds {
|
|||||||
type: EventType.ISSUE_SSH_CREDS;
|
type: EventType.ISSUE_SSH_CREDS;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
certType: SshCertType;
|
certType: SshCertType;
|
||||||
principals: string[];
|
principals: string[];
|
||||||
ttl: string;
|
ttl: string;
|
||||||
@@ -1456,6 +1480,80 @@ interface DeleteSshCertificateTemplate {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateSshHost {
|
||||||
|
type: EventType.CREATE_SSH_HOST;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
userCertTtl: string;
|
||||||
|
hostCertTtl: string;
|
||||||
|
loginMappings: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
userSshCaId: string;
|
||||||
|
hostSshCaId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSshHost {
|
||||||
|
type: EventType.UPDATE_SSH_HOST;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname?: string;
|
||||||
|
userCertTtl?: string;
|
||||||
|
hostCertTtl?: string;
|
||||||
|
loginMappings?: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
userSshCaId?: string;
|
||||||
|
hostSshCaId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshHost {
|
||||||
|
type: EventType.DELETE_SSH_HOST;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshHost {
|
||||||
|
type: EventType.GET_SSH_HOST;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueSshHostUserCert {
|
||||||
|
type: EventType.ISSUE_SSH_HOST_USER_CERT;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
loginUser: string;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueSshHostHostCert {
|
||||||
|
type: EventType.ISSUE_SSH_HOST_HOST_CERT;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
serialNumber: string;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCa {
|
interface CreateCa {
|
||||||
type: EventType.CREATE_CA;
|
type: EventType.CREATE_CA;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2313,6 +2411,63 @@ interface RegisterKmipServerEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetSecretRotationsEvent {
|
||||||
|
type: EventType.GET_SECRET_ROTATIONS;
|
||||||
|
metadata: {
|
||||||
|
type?: SecretRotation;
|
||||||
|
count: number;
|
||||||
|
rotationIds: string[];
|
||||||
|
secretPath?: string;
|
||||||
|
environment?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSecretRotationEvent {
|
||||||
|
type: EventType.GET_SECRET_ROTATION;
|
||||||
|
metadata: {
|
||||||
|
type: SecretRotation;
|
||||||
|
rotationId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSecretRotationCredentialsEvent {
|
||||||
|
type: EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS;
|
||||||
|
metadata: {
|
||||||
|
type: SecretRotation;
|
||||||
|
rotationId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSecretRotationEvent {
|
||||||
|
type: EventType.CREATE_SECRET_ROTATION;
|
||||||
|
metadata: Omit<TCreateSecretRotationV2DTO, "projectId"> & { rotationId: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSecretRotationEvent {
|
||||||
|
type: EventType.UPDATE_SECRET_ROTATION;
|
||||||
|
metadata: TUpdateSecretRotationV2DTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSecretRotationEvent {
|
||||||
|
type: EventType.DELETE_SECRET_ROTATION;
|
||||||
|
metadata: TDeleteSecretRotationV2DTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RotateSecretRotationEvent {
|
||||||
|
type: EventType.SECRET_ROTATION_ROTATE_SECRETS;
|
||||||
|
metadata: Pick<TSecretRotationV2Raw, "parameters" | "secretsMapping" | "type" | "connectionId" | "folderId"> & {
|
||||||
|
status: SecretRotationStatus;
|
||||||
|
rotationId: string;
|
||||||
|
jobId?: string | undefined;
|
||||||
|
occurredAt: Date;
|
||||||
|
message?: string | null | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -2419,6 +2574,12 @@ export type Event =
|
|||||||
| UpdateSshCertificateTemplate
|
| UpdateSshCertificateTemplate
|
||||||
| GetSshCertificateTemplate
|
| GetSshCertificateTemplate
|
||||||
| DeleteSshCertificateTemplate
|
| DeleteSshCertificateTemplate
|
||||||
|
| CreateSshHost
|
||||||
|
| UpdateSshHost
|
||||||
|
| DeleteSshHost
|
||||||
|
| GetSshHost
|
||||||
|
| IssueSshHostUserCert
|
||||||
|
| IssueSshHostHostCert
|
||||||
| CreateCa
|
| CreateCa
|
||||||
| GetCa
|
| GetCa
|
||||||
| UpdateCa
|
| UpdateCa
|
||||||
@@ -2524,4 +2685,11 @@ export type Event =
|
|||||||
| KmipOperationRegisterEvent
|
| KmipOperationRegisterEvent
|
||||||
| ProjectAccessRequestEvent
|
| ProjectAccessRequestEvent
|
||||||
| CreateSecretRequestEvent
|
| CreateSecretRequestEvent
|
||||||
| SecretApprovalRequestReview;
|
| SecretApprovalRequestReview
|
||||||
|
| GetSecretRotationsEvent
|
||||||
|
| GetSecretRotationEvent
|
||||||
|
| GetSecretRotationCredentialsEvent
|
||||||
|
| CreateSecretRotationEvent
|
||||||
|
| UpdateSecretRotationEvent
|
||||||
|
| DeleteSecretRotationEvent
|
||||||
|
| RotateSecretRotationEvent;
|
||||||
|
@@ -8,11 +8,13 @@ import { getDbConnectionHost } from "@app/lib/knex";
|
|||||||
|
|
||||||
export const verifyHostInputValidity = async (host: string, isGateway = false) => {
|
export const verifyHostInputValidity = async (host: string, isGateway = false) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
// if (appCfg.NODE_ENV === "development") return ["host.docker.internal"]; // incase you want to remove this check in dev
|
|
||||||
|
if (appCfg.isDevelopmentMode) return [host];
|
||||||
|
|
||||||
const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat(
|
const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat(
|
||||||
(appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)),
|
(appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)),
|
||||||
getDbConnectionHost(appCfg.REDIS_URL)
|
getDbConnectionHost(appCfg.REDIS_URL),
|
||||||
|
getDbConnectionHost(appCfg.AUDIT_LOGS_DB_CONNECTION_URI)
|
||||||
);
|
);
|
||||||
|
|
||||||
// get host db ip
|
// get host db ip
|
||||||
@@ -40,7 +42,7 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
|||||||
inputHostIps.push(...resolvedIps);
|
inputHostIps.push(...resolvedIps);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGateway) {
|
if (!isGateway && !appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP) {
|
||||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||||
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
||||||
}
|
}
|
||||||
|
@@ -39,7 +39,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: false,
|
||||||
secretRotation: true,
|
secretRotation: false,
|
||||||
caCrl: false,
|
caCrl: false,
|
||||||
instanceUserManagement: false,
|
instanceUserManagement: false,
|
||||||
externalKms: false,
|
externalKms: false,
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
// TODO(akhilmhdh): With tony find out the api structure and fill it here
|
// TODO(akhilmhdh): With tony find out the api structure and fill it here
|
||||||
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { CronJob } from "cron";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@@ -85,6 +86,20 @@ export const licenseServiceFactory = ({
|
|||||||
appCfg.LICENSE_KEY || ""
|
appCfg.LICENSE_KEY || ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const syncLicenseKeyOnPremFeatures = async (shouldThrow: boolean = false) => {
|
||||||
|
logger.info("Start syncing license key features");
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { currentPlan }
|
||||||
|
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan");
|
||||||
|
onPremFeatures = currentPlan;
|
||||||
|
logger.info("Successfully synchronized license key features");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to synchronize license key features");
|
||||||
|
if (shouldThrow) throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
if (appCfg.LICENSE_SERVER_KEY) {
|
if (appCfg.LICENSE_SERVER_KEY) {
|
||||||
@@ -98,10 +113,7 @@ export const licenseServiceFactory = ({
|
|||||||
if (appCfg.LICENSE_KEY) {
|
if (appCfg.LICENSE_KEY) {
|
||||||
const token = await licenseServerOnPremApi.refreshLicense();
|
const token = await licenseServerOnPremApi.refreshLicense();
|
||||||
if (token) {
|
if (token) {
|
||||||
const {
|
await syncLicenseKeyOnPremFeatures(true);
|
||||||
data: { currentPlan }
|
|
||||||
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan");
|
|
||||||
onPremFeatures = currentPlan;
|
|
||||||
instanceType = InstanceType.EnterpriseOnPrem;
|
instanceType = InstanceType.EnterpriseOnPrem;
|
||||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
||||||
isValidLicense = true;
|
isValidLicense = true;
|
||||||
@@ -147,6 +159,15 @@ export const licenseServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initializeBackgroundSync = async () => {
|
||||||
|
if (appCfg.LICENSE_KEY) {
|
||||||
|
logger.info("Setting up background sync process for refresh onPremFeatures");
|
||||||
|
const job = new CronJob("*/10 * * * *", syncLicenseKeyOnPremFeatures);
|
||||||
|
job.start();
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getPlan = async (orgId: string, projectId?: string) => {
|
const getPlan = async (orgId: string, projectId?: string) => {
|
||||||
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
|
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
|
||||||
try {
|
try {
|
||||||
@@ -662,6 +683,7 @@ export const licenseServiceFactory = ({
|
|||||||
getOrgTaxInvoices,
|
getOrgTaxInvoices,
|
||||||
getOrgTaxIds,
|
getOrgTaxIds,
|
||||||
addOrgTaxId,
|
addOrgTaxId,
|
||||||
delOrgTaxId
|
delOrgTaxId,
|
||||||
|
initializeBackgroundSync
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -56,7 +56,7 @@ export type TFeatureSet = {
|
|||||||
trial_end: null;
|
trial_end: null;
|
||||||
has_used_trial: true;
|
has_used_trial: true;
|
||||||
secretApproval: false;
|
secretApproval: false;
|
||||||
secretRotation: true;
|
secretRotation: false;
|
||||||
caCrl: false;
|
caCrl: false;
|
||||||
instanceUserManagement: false;
|
instanceUserManagement: false;
|
||||||
externalKms: false;
|
externalKms: false;
|
||||||
|
@@ -67,6 +67,14 @@ export enum ProjectPermissionGroupActions {
|
|||||||
GrantPrivileges = "grant-privileges"
|
GrantPrivileges = "grant-privileges"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSshHostActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
IssueHostCert = "issue-host-cert"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretSyncActions {
|
export enum ProjectPermissionSecretSyncActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@@ -77,6 +85,15 @@ export enum ProjectPermissionSecretSyncActions {
|
|||||||
RemoveSecrets = "remove-secrets"
|
RemoveSecrets = "remove-secrets"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSecretRotationActions {
|
||||||
|
Read = "read",
|
||||||
|
ReadGeneratedCredentials = "read-generated-credentials",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
RotateSecrets = "rotate-secrets"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionKmipActions {
|
export enum ProjectPermissionKmipActions {
|
||||||
CreateClients = "create-clients",
|
CreateClients = "create-clients",
|
||||||
UpdateClients = "update-clients",
|
UpdateClients = "update-clients",
|
||||||
@@ -112,6 +129,7 @@ export enum ProjectPermissionSub {
|
|||||||
SshCertificateAuthorities = "ssh-certificate-authorities",
|
SshCertificateAuthorities = "ssh-certificate-authorities",
|
||||||
SshCertificates = "ssh-certificates",
|
SshCertificates = "ssh-certificates",
|
||||||
SshCertificateTemplates = "ssh-certificate-templates",
|
SshCertificateTemplates = "ssh-certificate-templates",
|
||||||
|
SshHosts = "ssh-hosts",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
@@ -142,10 +160,19 @@ export type SecretImportSubjectFields = {
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SecretRotationsSubjectFields = {
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type IdentityManagementSubjectFields = {
|
export type IdentityManagementSubjectFields = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SshHostSubjectFields = {
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
@@ -184,7 +211,13 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [
|
||||||
|
ProjectPermissionSecretRotationActions,
|
||||||
|
(
|
||||||
|
| ProjectPermissionSub.SecretRotation
|
||||||
|
| (ForcedSubject<ProjectPermissionSub.SecretRotation> & SecretRotationsSubjectFields)
|
||||||
|
)
|
||||||
|
]
|
||||||
| [
|
| [
|
||||||
ProjectPermissionIdentityActions,
|
ProjectPermissionIdentityActions,
|
||||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||||
@@ -195,6 +228,10 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
|
| [
|
||||||
|
ProjectPermissionSshHostActions,
|
||||||
|
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||||
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||||
@@ -293,6 +330,21 @@ const IdentityManagementConditionSchema = z
|
|||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
|
const SshHostConditionSchema = z
|
||||||
|
.object({
|
||||||
|
hostname: z.union([
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
const GeneralPermissionSchema = [
|
const GeneralPermissionSchema = [
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||||
@@ -300,12 +352,6 @@ const GeneralPermissionSchema = [
|
|||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
z.object({
|
|
||||||
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
|
||||||
"Describe what action an entity can take."
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretRollback).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.SecretRollback).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
|
||||||
@@ -487,6 +533,12 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
|||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
...GeneralPermissionSchema
|
...GeneralPermissionSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -541,6 +593,26 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
"When specified, only matching conditions will be allowed to access given resource."
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
).optional()
|
).optional()
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SshHosts).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSshHostActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
),
|
||||||
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
|
conditions: SshHostConditionSchema.describe(
|
||||||
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
|
).optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||||
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretRotationActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
),
|
||||||
|
conditions: SecretConditionV1Schema.describe(
|
||||||
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
|
).optional()
|
||||||
|
}),
|
||||||
...GeneralPermissionSchema
|
...GeneralPermissionSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -554,7 +626,6 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.SecretFolders,
|
ProjectPermissionSub.SecretFolders,
|
||||||
ProjectPermissionSub.SecretImports,
|
ProjectPermissionSub.SecretImports,
|
||||||
ProjectPermissionSub.SecretApproval,
|
ProjectPermissionSub.SecretApproval,
|
||||||
ProjectPermissionSub.SecretRotation,
|
|
||||||
ProjectPermissionSub.Role,
|
ProjectPermissionSub.Role,
|
||||||
ProjectPermissionSub.Integrations,
|
ProjectPermissionSub.Integrations,
|
||||||
ProjectPermissionSub.Webhooks,
|
ProjectPermissionSub.Webhooks,
|
||||||
@@ -584,6 +655,17 @@ const buildAdminPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionSshHostActions.Edit,
|
||||||
|
ProjectPermissionSshHostActions.Read,
|
||||||
|
ProjectPermissionSshHostActions.Create,
|
||||||
|
ProjectPermissionSshHostActions.Delete,
|
||||||
|
ProjectPermissionSshHostActions.IssueHostCert
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.SshHosts
|
||||||
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionMemberActions.Create,
|
ProjectPermissionMemberActions.Create,
|
||||||
@@ -678,6 +760,18 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.Kmip
|
ProjectPermissionSub.Kmip
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionSecretRotationActions.Create,
|
||||||
|
ProjectPermissionSecretRotationActions.Edit,
|
||||||
|
ProjectPermissionSecretRotationActions.Delete,
|
||||||
|
ProjectPermissionSecretRotationActions.Read,
|
||||||
|
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
|
||||||
|
ProjectPermissionSecretRotationActions.RotateSecrets
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.SecretRotation
|
||||||
|
);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -727,7 +821,7 @@ const buildMemberPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
|
||||||
|
|
||||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||||
|
|
||||||
@@ -832,6 +926,8 @@ const buildMemberPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|
||||||
|
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionCmekActions.Create,
|
ProjectPermissionCmekActions.Create,
|
||||||
@@ -873,7 +969,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
|
||||||
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||||
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
|
@@ -594,6 +594,7 @@ export const scimServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
await orgMembershipDAL.updateById(
|
await orgMembershipDAL.updateById(
|
||||||
membership.id,
|
membership.id,
|
||||||
{
|
{
|
||||||
|
@@ -257,6 +257,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("id").withSchema("secVerTag")
|
db.ref("id").withSchema("secVerTag")
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretRotationV2SecretMapping,
|
||||||
|
`${TableName.SecretV2}.id`,
|
||||||
|
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||||
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
|
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
|
||||||
.select({
|
.select({
|
||||||
secVerTagId: "secVerTag.id",
|
secVerTagId: "secVerTag.id",
|
||||||
@@ -285,7 +290,8 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
);
|
)
|
||||||
|
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||||
const formatedDoc = sqlNestRelationships({
|
const formatedDoc = sqlNestRelationships({
|
||||||
data: doc,
|
data: doc,
|
||||||
key: "id",
|
key: "id",
|
||||||
@@ -304,14 +310,16 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
key: "secretId",
|
key: "secretId",
|
||||||
label: "secret" as const,
|
label: "secret" as const,
|
||||||
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) =>
|
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId, rotationId }) =>
|
||||||
secretId
|
secretId
|
||||||
? {
|
? {
|
||||||
id: secretId,
|
id: secretId,
|
||||||
version: orgSecVersion,
|
version: orgSecVersion,
|
||||||
key: orgSecKey,
|
key: orgSecKey,
|
||||||
encryptedValue: orgSecValue,
|
encryptedValue: orgSecValue,
|
||||||
encryptedComment: orgSecComment
|
encryptedComment: orgSecComment,
|
||||||
|
isRotatedSecret: Boolean(rotationId),
|
||||||
|
rotationId
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
},
|
},
|
||||||
|
@@ -262,7 +262,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
id: el.id,
|
id: el.id,
|
||||||
version: el.version,
|
version: el.version,
|
||||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||||
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||||
|
secretValue:
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
el.secret && el.secret.isRotatedSecret
|
||||||
|
? undefined
|
||||||
|
: el.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||||
|
: "",
|
||||||
secretComment: el.encryptedComment
|
secretComment: el.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||||
: "",
|
: "",
|
||||||
@@ -609,7 +616,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
inputSecrets: secretUpdationCommits.map((el) => {
|
inputSecrets: secretUpdationCommits.map((el) => {
|
||||||
const encryptedValue =
|
const encryptedValue =
|
||||||
typeof el.encryptedValue !== "undefined"
|
!el.secret?.isRotatedSecret && typeof el.encryptedValue !== "undefined"
|
||||||
? {
|
? {
|
||||||
encryptedValue: el.encryptedValue as Buffer,
|
encryptedValue: el.encryptedValue as Buffer,
|
||||||
references: el.encryptedValue
|
references: el.encryptedValue
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./mssql-credentials-rotation-constants";
|
||||||
|
export * from "./mssql-credentials-rotation-schemas";
|
||||||
|
export * from "./mssql-credentials-rotation-types";
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const MSSQL_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||||
|
name: "Microsoft SQL Server Credentials",
|
||||||
|
type: SecretRotation.MsSqlCredentials,
|
||||||
|
connection: AppConnection.MsSql,
|
||||||
|
template: {
|
||||||
|
createUserStatement: `-- Create login at the server level
|
||||||
|
CREATE LOGIN [infisical_user] WITH PASSWORD = 'my-password';
|
||||||
|
|
||||||
|
-- Grant server-level connect permission
|
||||||
|
GRANT CONNECT SQL TO [infisical_user];
|
||||||
|
|
||||||
|
-- Switch to the database where you want to create the user
|
||||||
|
USE my_database;
|
||||||
|
|
||||||
|
-- Create the database user mapped to the login
|
||||||
|
CREATE USER [infisical_user] FOR LOGIN [infisical_user];
|
||||||
|
|
||||||
|
-- Grant permissions to the user on the schema in this database
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [infisical_user];`,
|
||||||
|
secretsMapping: {
|
||||||
|
username: "MSSQL_DB_USERNAME",
|
||||||
|
password: "MSSQL_DB_PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import {
|
||||||
|
BaseCreateSecretRotationSchema,
|
||||||
|
BaseSecretRotationSchema,
|
||||||
|
BaseUpdateSecretRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||||
|
import {
|
||||||
|
SqlCredentialsRotationParametersSchema,
|
||||||
|
SqlCredentialsRotationSecretsMappingSchema,
|
||||||
|
SqlCredentialsRotationTemplateSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const MsSqlCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.MsSqlCredentials).extend({
|
||||||
|
type: z.literal(SecretRotation.MsSqlCredentials),
|
||||||
|
parameters: SqlCredentialsRotationParametersSchema,
|
||||||
|
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateMsSqlCredentialsRotationSchema = BaseCreateSecretRotationSchema(
|
||||||
|
SecretRotation.MsSqlCredentials
|
||||||
|
).extend({
|
||||||
|
parameters: SqlCredentialsRotationParametersSchema,
|
||||||
|
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateMsSqlCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
|
||||||
|
SecretRotation.MsSqlCredentials
|
||||||
|
).extend({
|
||||||
|
parameters: SqlCredentialsRotationParametersSchema.optional(),
|
||||||
|
secretsMapping: SqlCredentialsRotationSecretsMappingSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MsSqlCredentialsRotationListItemSchema = z.object({
|
||||||
|
name: z.literal("Microsoft SQL Server Credentials"),
|
||||||
|
connection: z.literal(AppConnection.MsSql),
|
||||||
|
type: z.literal(SecretRotation.MsSqlCredentials),
|
||||||
|
template: SqlCredentialsRotationTemplateSchema
|
||||||
|
});
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TMsSqlConnection } from "@app/services/app-connection/mssql";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateMsSqlCredentialsRotationSchema,
|
||||||
|
MsSqlCredentialsRotationListItemSchema,
|
||||||
|
MsSqlCredentialsRotationSchema
|
||||||
|
} from "./mssql-credentials-rotation-schemas";
|
||||||
|
|
||||||
|
export type TMsSqlCredentialsRotation = z.infer<typeof MsSqlCredentialsRotationSchema>;
|
||||||
|
|
||||||
|
export type TMsSqlCredentialsRotationInput = z.infer<typeof CreateMsSqlCredentialsRotationSchema>;
|
||||||
|
|
||||||
|
export type TMsSqlCredentialsRotationListItem = z.infer<typeof MsSqlCredentialsRotationListItemSchema>;
|
||||||
|
|
||||||
|
export type TMsSqlCredentialsRotationWithConnection = TMsSqlCredentialsRotation & {
|
||||||
|
connection: TMsSqlConnection;
|
||||||
|
};
|
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./postgres-credentials-rotation-constants";
|
||||||
|
export * from "./postgres-credentials-rotation-schemas";
|
||||||
|
export * from "./postgres-credentials-rotation-types";
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||||
|
name: "PostgreSQL Credentials",
|
||||||
|
type: SecretRotation.PostgresCredentials,
|
||||||
|
connection: AppConnection.Postgres,
|
||||||
|
template: {
|
||||||
|
createUserStatement: `-- create user role
|
||||||
|
CREATE USER infisical_user WITH ENCRYPTED PASSWORD 'temporary_password';
|
||||||
|
|
||||||
|
-- grant database connection permissions
|
||||||
|
GRANT CONNECT ON DATABASE my_database TO infisical_user;
|
||||||
|
|
||||||
|
-- grant relevant table permissions
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO infisical_user;`,
|
||||||
|
secretsMapping: {
|
||||||
|
username: "POSTGRES_DB_USERNAME",
|
||||||
|
password: "POSTGRES_DB_PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import {
|
||||||
|
BaseCreateSecretRotationSchema,
|
||||||
|
BaseSecretRotationSchema,
|
||||||
|
BaseUpdateSecretRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||||
|
import {
|
||||||
|
SqlCredentialsRotationParametersSchema,
|
||||||
|
SqlCredentialsRotationSecretsMappingSchema,
|
||||||
|
SqlCredentialsRotationTemplateSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const PostgresCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.PostgresCredentials).extend({
|
||||||
|
type: z.literal(SecretRotation.PostgresCredentials),
|
||||||
|
parameters: SqlCredentialsRotationParametersSchema,
|
||||||
|
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreatePostgresCredentialsRotationSchema = BaseCreateSecretRotationSchema(
|
||||||
|
SecretRotation.PostgresCredentials
|
||||||
|
).extend({
|
||||||
|
parameters: SqlCredentialsRotationParametersSchema,
|
||||||
|
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdatePostgresCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
|
||||||
|
SecretRotation.PostgresCredentials
|
||||||
|
).extend({
|
||||||
|
parameters: SqlCredentialsRotationParametersSchema.optional(),
|
||||||
|
secretsMapping: SqlCredentialsRotationSecretsMappingSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PostgresCredentialsRotationListItemSchema = z.object({
|
||||||
|
name: z.literal("PostgreSQL Credentials"),
|
||||||
|
connection: z.literal(AppConnection.Postgres),
|
||||||
|
type: z.literal(SecretRotation.PostgresCredentials),
|
||||||
|
template: SqlCredentialsRotationTemplateSchema
|
||||||
|
});
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TPostgresConnection } from "@app/services/app-connection/postgres";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreatePostgresCredentialsRotationSchema,
|
||||||
|
PostgresCredentialsRotationListItemSchema,
|
||||||
|
PostgresCredentialsRotationSchema
|
||||||
|
} from "./postgres-credentials-rotation-schemas";
|
||||||
|
|
||||||
|
export type TPostgresCredentialsRotation = z.infer<typeof PostgresCredentialsRotationSchema>;
|
||||||
|
|
||||||
|
export type TPostgresCredentialsRotationInput = z.infer<typeof CreatePostgresCredentialsRotationSchema>;
|
||||||
|
|
||||||
|
export type TPostgresCredentialsRotationListItem = z.infer<typeof PostgresCredentialsRotationListItemSchema>;
|
||||||
|
|
||||||
|
export type TPostgresCredentialsRotationWithConnection = TPostgresCredentialsRotation & {
|
||||||
|
connection: TPostgresConnection;
|
||||||
|
};
|
@@ -0,0 +1,467 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { TSecretRotationsV2 } from "@app/db/schemas/secret-rotations-v2";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import {
|
||||||
|
buildFindFilter,
|
||||||
|
ormify,
|
||||||
|
prependTableNameToFindFilter,
|
||||||
|
selectAllTableCols,
|
||||||
|
sqlNestRelationships,
|
||||||
|
TFindOpt
|
||||||
|
} from "@app/lib/knex";
|
||||||
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
|
export type TSecretRotationV2DALFactory = ReturnType<typeof secretRotationV2DALFactory>;
|
||||||
|
|
||||||
|
type TSecretRotationFindFilter = Parameters<typeof buildFindFilter<TSecretRotationsV2>>[0];
|
||||||
|
type TSecretRotationFindOptions = TFindOpt<TSecretRotationsV2, true, "name">;
|
||||||
|
|
||||||
|
const baseSecretRotationV2Query = ({
|
||||||
|
filter = {},
|
||||||
|
options,
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
db: TDbClient;
|
||||||
|
filter?: { projectId?: string } & TSecretRotationFindFilter;
|
||||||
|
options?: TSecretRotationFindOptions;
|
||||||
|
tx?: Knex;
|
||||||
|
}) => {
|
||||||
|
const { projectId, ...filters } = filter;
|
||||||
|
|
||||||
|
const query = (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||||
|
.join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.join(TableName.AppConnection, `${TableName.SecretRotationV2}.connectionId`, `${TableName.AppConnection}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.SecretRotationV2))
|
||||||
|
.select(
|
||||||
|
// environment
|
||||||
|
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
|
// entire connection
|
||||||
|
db.ref("name").withSchema(TableName.AppConnection).as("connectionName"),
|
||||||
|
db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"),
|
||||||
|
db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"),
|
||||||
|
db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"),
|
||||||
|
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
|
||||||
|
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||||
|
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||||
|
db
|
||||||
|
.ref("isPlatformManagedCredentials")
|
||||||
|
.withSchema(TableName.AppConnection)
|
||||||
|
.as("connectionIsPlatformManagedCredentials")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||||
|
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filters)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
void query.where(`${TableName.Environment}.projectId`, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
const { offset, limit, sort, count, countDistinct } = options;
|
||||||
|
if (countDistinct) {
|
||||||
|
void query.countDistinct(countDistinct);
|
||||||
|
} else if (count) {
|
||||||
|
void query.select(db.raw("COUNT(*) OVER() AS count"));
|
||||||
|
void query.select("*");
|
||||||
|
}
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.offset(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRotationV2Query>>[number]>(
|
||||||
|
secretRotation: T,
|
||||||
|
folder: Awaited<ReturnType<TSecretFolderDALFactory["findSecretPathByFolderIds"]>>[number]
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
envId,
|
||||||
|
envName,
|
||||||
|
envSlug,
|
||||||
|
connectionApp,
|
||||||
|
connectionName,
|
||||||
|
connectionId,
|
||||||
|
connectionOrgId,
|
||||||
|
connectionEncryptedCredentials,
|
||||||
|
connectionMethod,
|
||||||
|
connectionDescription,
|
||||||
|
connectionCreatedAt,
|
||||||
|
connectionUpdatedAt,
|
||||||
|
connectionVersion,
|
||||||
|
connectionIsPlatformManagedCredentials,
|
||||||
|
...el
|
||||||
|
} = secretRotation;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
connectionId,
|
||||||
|
environment: { id: envId, name: envName, slug: envSlug },
|
||||||
|
connection: {
|
||||||
|
app: connectionApp,
|
||||||
|
id: connectionId,
|
||||||
|
name: connectionName,
|
||||||
|
orgId: connectionOrgId,
|
||||||
|
encryptedCredentials: connectionEncryptedCredentials,
|
||||||
|
method: connectionMethod,
|
||||||
|
description: connectionDescription,
|
||||||
|
createdAt: connectionCreatedAt,
|
||||||
|
updatedAt: connectionUpdatedAt,
|
||||||
|
version: connectionVersion,
|
||||||
|
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
|
||||||
|
},
|
||||||
|
folder: {
|
||||||
|
id: folder!.id,
|
||||||
|
path: folder!.path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const secretRotationV2DALFactory = (
|
||||||
|
db: TDbClient,
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">
|
||||||
|
) => {
|
||||||
|
const secretRotationV2Orm = ormify(db, TableName.SecretRotationV2);
|
||||||
|
const secretRotationV2SecretMappingOrm = ormify(db, TableName.SecretRotationV2SecretMapping);
|
||||||
|
|
||||||
|
const find = async (
|
||||||
|
filter: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string },
|
||||||
|
options?: TSecretRotationFindOptions,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const secretRotations = await baseSecretRotationV2Query({ filter, db, tx, options });
|
||||||
|
|
||||||
|
if (!secretRotations.length) return [];
|
||||||
|
|
||||||
|
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
|
||||||
|
filter.projectId,
|
||||||
|
secretRotations.map((rotation) => rotation.folderId),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
|
||||||
|
|
||||||
|
foldersWithPath.forEach((folder) => {
|
||||||
|
if (folder) folderRecord[folder.id] = folder;
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretRotations.map((rotation) => expandSecretRotation(rotation, folderRecord[rotation.folderId]));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find - Secret Rotation V2" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWithMappedSecretsCount = async (
|
||||||
|
{
|
||||||
|
search,
|
||||||
|
projectId,
|
||||||
|
...filter
|
||||||
|
}: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string; search?: string },
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||||
|
.join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.join(
|
||||||
|
TableName.SecretRotationV2SecretMapping,
|
||||||
|
`${TableName.SecretRotationV2SecretMapping}.rotationId`,
|
||||||
|
`${TableName.SecretRotationV2}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.SecretV2, `${TableName.SecretRotationV2SecretMapping}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
|
.where(`${TableName.Environment}.projectId`, projectId)
|
||||||
|
.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filter)))
|
||||||
|
.countDistinct(`${TableName.SecretRotationV2}.name`);
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
void query.where((qb) => {
|
||||||
|
void qb
|
||||||
|
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query;
|
||||||
|
|
||||||
|
// @ts-expect-error knex infers wrong type...
|
||||||
|
return Number(result[0]?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWithMappedSecrets = async (
|
||||||
|
{ search, ...filter }: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string; search?: string },
|
||||||
|
options?: TSecretRotationFindOptions,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const extendedQuery = baseSecretRotationV2Query({ filter, db, tx, options })
|
||||||
|
.join(
|
||||||
|
TableName.SecretRotationV2SecretMapping,
|
||||||
|
`${TableName.SecretRotationV2SecretMapping}.rotationId`,
|
||||||
|
`${TableName.SecretRotationV2}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretRotationV2SecretMapping}.secretId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretV2JnTag,
|
||||||
|
`${TableName.SecretV2}.id`,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretTag,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||||
|
`${TableName.SecretTag}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SecretV2).as("secretId"),
|
||||||
|
db.ref("key").withSchema(TableName.SecretV2).as("secretKey"),
|
||||||
|
db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"),
|
||||||
|
db.ref("type").withSchema(TableName.SecretV2).as("secretType"),
|
||||||
|
db.ref("encryptedValue").withSchema(TableName.SecretV2).as("secretEncryptedValue"),
|
||||||
|
db.ref("encryptedComment").withSchema(TableName.SecretV2).as("secretEncryptedComment"),
|
||||||
|
db.ref("reminderNote").withSchema(TableName.SecretV2).as("secretReminderNote"),
|
||||||
|
db.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("secretReminderRepeatDays"),
|
||||||
|
db.ref("skipMultilineEncoding").withSchema(TableName.SecretV2).as("secretSkipMultilineEncoding"),
|
||||||
|
db.ref("metadata").withSchema(TableName.SecretV2).as("secretMetadata"),
|
||||||
|
db.ref("userId").withSchema(TableName.SecretV2).as("secretUserId"),
|
||||||
|
db.ref("folderId").withSchema(TableName.SecretV2).as("secretFolderId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.SecretV2).as("secretCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.SecretV2).as("secretUpdatedAt"),
|
||||||
|
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||||
|
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||||
|
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
void extendedQuery.where((query) => {
|
||||||
|
void query
|
||||||
|
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretRotations = await extendedQuery;
|
||||||
|
|
||||||
|
if (!secretRotations.length) return [];
|
||||||
|
|
||||||
|
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
|
||||||
|
filter.projectId,
|
||||||
|
secretRotations.map((rotation) => rotation.folderId),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
|
||||||
|
|
||||||
|
foldersWithPath.forEach((folder) => {
|
||||||
|
if (folder) folderRecord[folder.id] = folder;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sqlNestRelationships({
|
||||||
|
data: secretRotations,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (rotation) => expandSecretRotation(rotation, folderRecord[rotation.folderId]),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "secretId",
|
||||||
|
label: "secrets" as const,
|
||||||
|
mapper: ({
|
||||||
|
secretId,
|
||||||
|
secretKey,
|
||||||
|
secretVersion,
|
||||||
|
secretType,
|
||||||
|
secretEncryptedValue,
|
||||||
|
secretEncryptedComment,
|
||||||
|
secretReminderNote,
|
||||||
|
secretReminderRepeatDays,
|
||||||
|
secretSkipMultilineEncoding,
|
||||||
|
secretMetadata,
|
||||||
|
secretUserId,
|
||||||
|
secretFolderId,
|
||||||
|
secretCreatedAt,
|
||||||
|
secretUpdatedAt,
|
||||||
|
id
|
||||||
|
}) => ({
|
||||||
|
id: secretId,
|
||||||
|
key: secretKey,
|
||||||
|
version: secretVersion,
|
||||||
|
type: secretType,
|
||||||
|
encryptedValue: secretEncryptedValue,
|
||||||
|
encryptedComment: secretEncryptedComment,
|
||||||
|
reminderNote: secretReminderNote,
|
||||||
|
reminderRepeatDays: secretReminderRepeatDays,
|
||||||
|
skipMultilineEncoding: secretSkipMultilineEncoding,
|
||||||
|
metadata: secretMetadata,
|
||||||
|
userId: secretUserId,
|
||||||
|
folderId: secretFolderId,
|
||||||
|
createdAt: secretCreatedAt,
|
||||||
|
updatedAt: secretUpdatedAt,
|
||||||
|
rotationId: id,
|
||||||
|
isRotatedSecret: true
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name: slug
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "secretMetadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find with Mapped Secrets - Secret Rotation V2" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const secretRotation = await baseSecretRotationV2Query({
|
||||||
|
filter: { id },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first();
|
||||||
|
|
||||||
|
if (secretRotation) {
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||||
|
secretRotation.projectId,
|
||||||
|
[secretRotation.folderId],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return expandSecretRotation(secretRotation, folderWithPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by ID - Secret Rotation V2" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (data: Parameters<(typeof secretRotationV2Orm)["create"]>[0], tx?: Knex) => {
|
||||||
|
const rotation = await secretRotationV2Orm.create(data, tx);
|
||||||
|
|
||||||
|
const secretRotation = (await baseSecretRotationV2Query({
|
||||||
|
filter: { id: rotation.id },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first())!;
|
||||||
|
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||||
|
secretRotation.projectId,
|
||||||
|
[secretRotation.folderId],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return expandSecretRotation(secretRotation, folderWithPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateById = async (
|
||||||
|
rotationId: string,
|
||||||
|
data: Parameters<(typeof secretRotationV2Orm)["updateById"]>[1],
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
const rotation = await secretRotationV2Orm.updateById(rotationId, data, tx);
|
||||||
|
|
||||||
|
const secretRotation = (await baseSecretRotationV2Query({
|
||||||
|
filter: { id: rotation.id },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first())!;
|
||||||
|
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||||
|
secretRotation.projectId,
|
||||||
|
[secretRotation.folderId],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return expandSecretRotation(secretRotation, folderWithPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteById = async (rotationId: string, tx?: Knex) => {
|
||||||
|
const secretRotation = (await baseSecretRotationV2Query({
|
||||||
|
filter: { id: rotationId },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first())!;
|
||||||
|
|
||||||
|
await secretRotationV2Orm.deleteById(rotationId, tx);
|
||||||
|
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||||
|
secretRotation.projectId,
|
||||||
|
[secretRotation.folderId],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return expandSecretRotation(secretRotation, folderWithPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOne = async (filter: Parameters<(typeof secretRotationV2Orm)["findOne"]>[0], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const secretRotation = await baseSecretRotationV2Query({ filter, db, tx }).first();
|
||||||
|
|
||||||
|
if (secretRotation) {
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(
|
||||||
|
secretRotation.projectId,
|
||||||
|
[secretRotation.folderId],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return expandSecretRotation(secretRotation, folderWithPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find One - Secret Rotation V2" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSecretRotationsToQueue = async (rotateBy: Date, tx?: Knex) => {
|
||||||
|
const secretRotations = await (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||||
|
.where(`${TableName.SecretRotationV2}.isAutoRotationEnabled`, true)
|
||||||
|
.whereNotNull(`${TableName.SecretRotationV2}.nextRotationAt`)
|
||||||
|
.andWhereRaw(`"nextRotationAt" <= ?`, [rotateBy])
|
||||||
|
.select(selectAllTableCols(TableName.SecretRotationV2));
|
||||||
|
|
||||||
|
return secretRotations;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...secretRotationV2Orm,
|
||||||
|
find,
|
||||||
|
create,
|
||||||
|
findById,
|
||||||
|
updateById,
|
||||||
|
deleteById,
|
||||||
|
findOne,
|
||||||
|
insertSecretMappings: secretRotationV2SecretMappingOrm.insertMany,
|
||||||
|
findWithMappedSecrets,
|
||||||
|
findWithMappedSecretsCount,
|
||||||
|
findSecretRotationsToQueue
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,9 @@
|
|||||||
|
export enum SecretRotation {
|
||||||
|
PostgresCredentials = "postgres-credentials",
|
||||||
|
MsSqlCredentials = "mssql-credentials"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SecretRotationStatus {
|
||||||
|
Success = "success",
|
||||||
|
Failed = "failed"
|
||||||
|
}
|
@@ -0,0 +1,222 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||||
|
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||||
|
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||||
|
import { TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service";
|
||||||
|
import {
|
||||||
|
TSecretRotationV2,
|
||||||
|
TSecretRotationV2GeneratedCredentials,
|
||||||
|
TSecretRotationV2ListItem,
|
||||||
|
TSecretRotationV2Raw
|
||||||
|
} from "./secret-rotation-v2-types";
|
||||||
|
|
||||||
|
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||||
|
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||||
|
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listSecretRotationOptions = () => {
|
||||||
|
return Object.values(SECRET_ROTATION_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextUTCDayInterval = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return new Date(
|
||||||
|
Date.UTC(
|
||||||
|
now.getUTCFullYear(),
|
||||||
|
now.getUTCMonth(),
|
||||||
|
now.getUTCDate() + 1, // Add 1 day to get tomorrow
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextUTCMinuteInterval = ({ minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => {
|
||||||
|
const now = new Date();
|
||||||
|
return new Date(
|
||||||
|
Date.UTC(
|
||||||
|
now.getUTCFullYear(),
|
||||||
|
now.getUTCMonth(),
|
||||||
|
now.getUTCDate(),
|
||||||
|
now.getUTCHours(),
|
||||||
|
now.getUTCMinutes() + 1, // Add 1 minute to get the next minute
|
||||||
|
minutes, // use minutes as seconds in dev
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNextUtcRotationInterval = (rotateAtUtc?: TSecretRotationV2["rotateAtUtc"]) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
if (appCfg.isRotationDevelopmentMode) {
|
||||||
|
return getNextUTCMinuteInterval(rotateAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getNextUTCDayInterval(rotateAtUtc);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptSecretRotationCredentials = async ({
|
||||||
|
projectId,
|
||||||
|
generatedCredentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
generatedCredentials: TSecretRotationV2GeneratedCredentials;
|
||||||
|
kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"];
|
||||||
|
}) => {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||||
|
plainText: Buffer.from(JSON.stringify(generatedCredentials))
|
||||||
|
});
|
||||||
|
|
||||||
|
return encryptedCredentialsBlob;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptSecretRotationCredentials = async ({
|
||||||
|
projectId,
|
||||||
|
encryptedGeneratedCredentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
encryptedGeneratedCredentials: Buffer;
|
||||||
|
kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"];
|
||||||
|
}) => {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPlainTextBlob = decryptor({
|
||||||
|
cipherTextBlob: encryptedGeneratedCredentials
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(decryptedPlainTextBlob.toString()) as TSecretRotationV2GeneratedCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSecretRotationRotateSecretJobOptions = ({
|
||||||
|
id,
|
||||||
|
nextRotationAt
|
||||||
|
}: Pick<TSecretRotationV2Raw, "id" | "nextRotationAt">) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
jobId: `secret-rotation-v2-rotate-${id}`,
|
||||||
|
retryLimit: appCfg.isRotationDevelopmentMode ? 3 : 5,
|
||||||
|
retryBackoff: true,
|
||||||
|
startAfter: nextRotationAt ?? undefined
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateNextRotationAt = ({
|
||||||
|
rotateAtUtc,
|
||||||
|
isAutoRotationEnabled,
|
||||||
|
rotationInterval,
|
||||||
|
rotationStatus,
|
||||||
|
isManualRotation,
|
||||||
|
...params
|
||||||
|
}: Pick<
|
||||||
|
TSecretRotationV2,
|
||||||
|
"isAutoRotationEnabled" | "lastRotatedAt" | "rotateAtUtc" | "rotationInterval" | "rotationStatus"
|
||||||
|
> & { isManualRotation: boolean }) => {
|
||||||
|
if (!isAutoRotationEnabled) return null;
|
||||||
|
|
||||||
|
if (rotationStatus === SecretRotationStatus.Failed) {
|
||||||
|
return getNextUtcRotationInterval(rotateAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastRotatedAt = new Date(params.lastRotatedAt);
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
if (appCfg.isRotationDevelopmentMode) {
|
||||||
|
// treat interval as minute
|
||||||
|
const nextRotation = new Date(lastRotatedAt.getTime() + rotationInterval * 60 * 1000);
|
||||||
|
|
||||||
|
// in development mode we use rotateAtUtc.minutes as seconds
|
||||||
|
nextRotation.setUTCSeconds(rotateAtUtc.minutes);
|
||||||
|
nextRotation.setUTCMilliseconds(0);
|
||||||
|
|
||||||
|
// If creation/manual rotation seconds are after the configured seconds we pad an additional minute
|
||||||
|
// to ensure a full interval has elapsed before rotation
|
||||||
|
if (isManualRotation && lastRotatedAt.getUTCSeconds() >= rotateAtUtc.minutes) {
|
||||||
|
nextRotation.setUTCMinutes(nextRotation.getUTCMinutes() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// production mode - rotationInterval = days
|
||||||
|
|
||||||
|
const nextRotation = new Date(lastRotatedAt);
|
||||||
|
|
||||||
|
nextRotation.setUTCHours(rotateAtUtc.hours);
|
||||||
|
nextRotation.setUTCMinutes(rotateAtUtc.minutes);
|
||||||
|
nextRotation.setUTCSeconds(0);
|
||||||
|
nextRotation.setUTCMilliseconds(0);
|
||||||
|
|
||||||
|
// If creation/manual rotation was after the daily rotation time,
|
||||||
|
// we need pad an additional day to ensure full rotation interval
|
||||||
|
if (
|
||||||
|
isManualRotation &&
|
||||||
|
(lastRotatedAt.getUTCHours() > rotateAtUtc.hours ||
|
||||||
|
(lastRotatedAt.getUTCHours() === rotateAtUtc.hours && lastRotatedAt.getUTCMinutes() >= rotateAtUtc.minutes))
|
||||||
|
) {
|
||||||
|
nextRotation.setUTCDate(nextRotation.getUTCDate() + rotationInterval + 1);
|
||||||
|
} else {
|
||||||
|
nextRotation.setUTCDate(nextRotation.getUTCDate() + rotationInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextRotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandSecretRotation = async (
|
||||||
|
{ encryptedLastRotationMessage, ...secretRotation }: TSecretRotationV2Raw,
|
||||||
|
kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"]
|
||||||
|
) => {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: secretRotation.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastRotationMessage = encryptedLastRotationMessage
|
||||||
|
? decryptor({
|
||||||
|
cipherTextBlob: encryptedLastRotationMessage
|
||||||
|
}).toString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...secretRotation,
|
||||||
|
lastRotationMessage
|
||||||
|
} as TSecretRotationV2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_MESSAGE_LENGTH = 1024;
|
||||||
|
|
||||||
|
export const parseRotationErrorMessage = (err: unknown): string => {
|
||||||
|
let errorMessage = `Infisical encountered an issue while generating credentials with the configured inputs: `;
|
||||||
|
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
errorMessage += err?.response?.data
|
||||||
|
? JSON.stringify(err?.response?.data)
|
||||||
|
: err?.message ?? "An unknown error occurred.";
|
||||||
|
} else {
|
||||||
|
errorMessage += (err as Error)?.message || "An unknown error occurred.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorMessage.length <= MAX_MESSAGE_LENGTH
|
||||||
|
? errorMessage
|
||||||
|
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
|
||||||
|
};
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||||
|
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||||
|
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||||
|
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||||
|
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
|
||||||
|
};
|
@@ -0,0 +1,193 @@
|
|||||||
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
|
import { TSecretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import {
|
||||||
|
getNextUtcRotationInterval,
|
||||||
|
getSecretRotationRotateSecretJobOptions
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns";
|
||||||
|
import { SECRET_ROTATION_NAME_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||||
|
import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
|
||||||
|
import {
|
||||||
|
TSecretRotationRotateSecretsJobPayload,
|
||||||
|
TSecretRotationSendNotificationJobPayload
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
|
type TSecretRotationV2QueueServiceFactoryDep = {
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
secretRotationV2DAL: Pick<TSecretRotationV2DALFactory, "findSecretRotationsToQueue" | "findById">;
|
||||||
|
secretRotationV2Service: Pick<TSecretRotationV2ServiceFactory, "rotateGeneratedCredentials">;
|
||||||
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const secretRotationV2QueueServiceFactory = async ({
|
||||||
|
queueService,
|
||||||
|
secretRotationV2DAL,
|
||||||
|
secretRotationV2Service,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
smtpService
|
||||||
|
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
if (appCfg.isRotationDevelopmentMode) {
|
||||||
|
logger.warn("Secret Rotation V2 is in development mode.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await queueService.startPg<QueueName.SecretRotationV2>(
|
||||||
|
QueueJobs.SecretRotationV2QueueRotations,
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const rotateBy = getNextUtcRotationInterval();
|
||||||
|
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
const secretRotations = await secretRotationV2DAL.findSecretRotationsToQueue(rotateBy);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`secretRotationV2Queue: Queue Rotations [currentTime=${currentTime.toISOString()}] [rotateBy=${rotateBy.toISOString()}] [count=${
|
||||||
|
secretRotations.length
|
||||||
|
}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const rotation of secretRotations) {
|
||||||
|
logger.info(
|
||||||
|
`secretRotationV2Queue: Queue Rotation [rotationId=${rotation.id}] [lastRotatedAt=${new Date(
|
||||||
|
rotation.lastRotatedAt
|
||||||
|
).toISOString()}] [rotateAt=${new Date(rotation.nextRotationAt!).toISOString()}]`
|
||||||
|
);
|
||||||
|
await queueService.queuePg(
|
||||||
|
QueueJobs.SecretRotationV2RotateSecrets,
|
||||||
|
{
|
||||||
|
rotationId: rotation.id,
|
||||||
|
queuedAt: currentTime
|
||||||
|
},
|
||||||
|
getSecretRotationRotateSecretJobOptions(rotation)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "secretRotationV2Queue: Queue Rotations Error:");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchSize: 1,
|
||||||
|
workerCount: 1,
|
||||||
|
pollingIntervalSeconds: appCfg.isRotationDevelopmentMode ? 0.5 : 30
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await queueService.startPg<QueueName.SecretRotationV2>(
|
||||||
|
QueueJobs.SecretRotationV2RotateSecrets,
|
||||||
|
async ([job]) => {
|
||||||
|
const { rotationId, queuedAt, isManualRotation } = job.data as TSecretRotationRotateSecretsJobPayload;
|
||||||
|
const { retryCount, retryLimit } = job;
|
||||||
|
|
||||||
|
const logDetails = `[rotationId=${rotationId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||||
|
|
||||||
|
if (!secretRotation) throw new Error(`Secret rotation ${rotationId} not found`);
|
||||||
|
|
||||||
|
if (!secretRotation.isAutoRotationEnabled) {
|
||||||
|
logger.info(`secretRotationV2Queue: Skipping Rotation - Auto-Rotation Disabled Since Queue ${logDetails}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date(secretRotation.lastRotatedAt).getTime() >= new Date(queuedAt).getTime()) {
|
||||||
|
// rotated since being queued, skip rotation
|
||||||
|
logger.info(`secretRotationV2Queue: Skipping Rotation - Rotated Since Queue ${logDetails}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await secretRotationV2Service.rotateGeneratedCredentials(secretRotation, {
|
||||||
|
jobId: job.id,
|
||||||
|
shouldSendNotification: true,
|
||||||
|
isFinalAttempt: retryCount === retryLimit,
|
||||||
|
isManualRotation
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`secretRotationV2Queue: Secrets Rotated ${logDetails}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `secretRotationV2Queue: Failed to Rotate Secrets ${logDetails}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchSize: 1,
|
||||||
|
workerCount: 2,
|
||||||
|
pollingIntervalSeconds: 0.5
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await queueService.startPg<QueueName.SecretRotationV2>(
|
||||||
|
QueueJobs.SecretRotationV2SendNotification,
|
||||||
|
async ([job]) => {
|
||||||
|
const { secretRotation } = job.data as TSecretRotationSendNotificationJobPayload;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
name: rotationName,
|
||||||
|
type,
|
||||||
|
projectId,
|
||||||
|
lastRotationAttemptedAt,
|
||||||
|
folder,
|
||||||
|
environment,
|
||||||
|
id: rotationId
|
||||||
|
} = secretRotation;
|
||||||
|
|
||||||
|
logger.info(`secretRotationV2Queue: Sending Status Notification [rotationId=${rotationId}]`);
|
||||||
|
|
||||||
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
|
||||||
|
const projectAdmins = projectMembers.filter((member) =>
|
||||||
|
member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
|
||||||
|
);
|
||||||
|
|
||||||
|
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||||
|
template: SmtpTemplates.SecretRotationFailed,
|
||||||
|
subjectLine: `Secret Rotation Failed`,
|
||||||
|
substitutions: {
|
||||||
|
rotationName,
|
||||||
|
rotationType,
|
||||||
|
content: `Your ${rotationType} Rotation failed to rotate during it's scheduled rotation. The last rotation attempt occurred at ${new Date(
|
||||||
|
lastRotationAttemptedAt
|
||||||
|
).toISOString()}. Please check the rotation status in Infisical for more details.`,
|
||||||
|
secretPath: folder.path,
|
||||||
|
environment: environment.name,
|
||||||
|
projectName: project.name,
|
||||||
|
rotationUrl: encodeURI(`${appCfg.SITE_URL}/secret-manager/${projectId}/secrets/${environment.slug}`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
`secretRotationV2Queue: Failed to Send Status Notification [rotationId=${secretRotation.id}]`
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchSize: 1,
|
||||||
|
workerCount: 2,
|
||||||
|
pollingIntervalSeconds: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await queueService.schedulePg(
|
||||||
|
QueueJobs.SecretRotationV2QueueRotations,
|
||||||
|
appCfg.isRotationDevelopmentMode ? "* * * * *" : "0 0 * * *",
|
||||||
|
undefined,
|
||||||
|
{ tz: "UTC" }
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretRotationsV2Schema } from "@app/db/schemas/secret-rotations-v2";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||||
|
import { SecretRotations } from "@app/lib/api-docs";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
|
||||||
|
const RotateAtUtcSchema = z.object({
|
||||||
|
hours: z.number().min(0).max(23),
|
||||||
|
minutes: z.number().min(0).max(59)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BaseSecretRotationSchema = (type: SecretRotation) =>
|
||||||
|
SecretRotationsV2Schema.omit({
|
||||||
|
encryptedGeneratedCredentials: true,
|
||||||
|
encryptedLastRotationMessage: true,
|
||||||
|
rotateAtUtc: true,
|
||||||
|
// unique to provider
|
||||||
|
type: true,
|
||||||
|
parameters: true,
|
||||||
|
secretMappings: true
|
||||||
|
}).extend({
|
||||||
|
connection: z.object({
|
||||||
|
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string().uuid()
|
||||||
|
}),
|
||||||
|
environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }),
|
||||||
|
projectId: z.string(),
|
||||||
|
folder: z.object({ id: z.string(), path: z.string() }),
|
||||||
|
rotateAtUtc: RotateAtUtcSchema,
|
||||||
|
lastRotationMessage: z.string().nullish()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BaseCreateSecretRotationSchema = (type: SecretRotation) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(SecretRotations.CREATE(type).name),
|
||||||
|
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.CREATE(type).projectId),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(SecretRotations.CREATE(type).description),
|
||||||
|
connectionId: z.string().uuid().describe(SecretRotations.CREATE(type).connectionId),
|
||||||
|
environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.CREATE(type).environment),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Secret path required")
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(SecretRotations.CREATE(type).secretPath),
|
||||||
|
isAutoRotationEnabled: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(SecretRotations.CREATE(type).isAutoRotationEnabled),
|
||||||
|
rotationInterval: z.coerce.number().min(1).describe(SecretRotations.CREATE(type).rotationInterval),
|
||||||
|
rotateAtUtc: RotateAtUtcSchema.optional().describe(SecretRotations.CREATE(type).rotateAtUtc)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BaseUpdateSecretRotationSchema = (type: SecretRotation) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(SecretRotations.UPDATE(type).name).optional(),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(SecretRotations.UPDATE(type).description),
|
||||||
|
isAutoRotationEnabled: z.boolean().optional().describe(SecretRotations.UPDATE(type).isAutoRotationEnabled),
|
||||||
|
rotationInterval: z.coerce.number().min(1).optional().describe(SecretRotations.UPDATE(type).rotationInterval),
|
||||||
|
rotateAtUtc: RotateAtUtcSchema.optional().describe(SecretRotations.UPDATE(type).rotateAtUtc)
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
|||||||
|
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TMsSqlCredentialsRotation,
|
||||||
|
TMsSqlCredentialsRotationInput,
|
||||||
|
TMsSqlCredentialsRotationListItem,
|
||||||
|
TMsSqlCredentialsRotationWithConnection
|
||||||
|
} from "./mssql-credentials";
|
||||||
|
import {
|
||||||
|
TPostgresCredentialsRotation,
|
||||||
|
TPostgresCredentialsRotationInput,
|
||||||
|
TPostgresCredentialsRotationListItem,
|
||||||
|
TPostgresCredentialsRotationWithConnection
|
||||||
|
} from "./postgres-credentials";
|
||||||
|
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||||
|
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||||
|
|
||||||
|
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation;
|
||||||
|
|
||||||
|
export type TSecretRotationV2WithConnection =
|
||||||
|
| TPostgresCredentialsRotationWithConnection
|
||||||
|
| TMsSqlCredentialsRotationWithConnection;
|
||||||
|
|
||||||
|
export type TSecretRotationV2GeneratedCredentials = TSqlCredentialsRotationGeneratedCredentials;
|
||||||
|
|
||||||
|
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput;
|
||||||
|
|
||||||
|
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem;
|
||||||
|
|
||||||
|
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||||
|
|
||||||
|
export type TListSecretRotationsV2ByProjectId = {
|
||||||
|
projectId: string;
|
||||||
|
type?: SecretRotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFindSecretRotationV2ByIdDTO = {
|
||||||
|
rotationId: string;
|
||||||
|
type: SecretRotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRotateSecretRotationV2 = TFindSecretRotationV2ByIdDTO & { auditLogInfo: AuditLogInfo };
|
||||||
|
|
||||||
|
export type TRotateAtUtc = { hours: number; minutes: number };
|
||||||
|
|
||||||
|
export type TFindSecretRotationV2ByNameDTO = {
|
||||||
|
rotationName: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
projectId: string;
|
||||||
|
type: SecretRotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSecretRotationV2DTO = Pick<
|
||||||
|
TSecretRotationV2,
|
||||||
|
"parameters" | "secretsMapping" | "description" | "rotationInterval" | "name" | "connectionId" | "projectId"
|
||||||
|
> & {
|
||||||
|
type: SecretRotation;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
isAutoRotationEnabled?: boolean;
|
||||||
|
rotateAtUtc?: TRotateAtUtc;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSecretRotationV2DTO = Partial<
|
||||||
|
Omit<TCreateSecretRotationV2DTO, "projectId" | "connectionId" | "secretPath" | "environment">
|
||||||
|
> & {
|
||||||
|
rotationId: string;
|
||||||
|
type: SecretRotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSecretRotationV2DTO = {
|
||||||
|
type: SecretRotation;
|
||||||
|
rotationId: string;
|
||||||
|
deleteSecrets: boolean;
|
||||||
|
revokeGeneratedCredentials: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetDashboardSecretRotationV2Count = {
|
||||||
|
search?: string;
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environments: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetDashboardSecretRotationsV2 = {
|
||||||
|
search?: string;
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environments: string[];
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TQuickSearchSecretRotationsV2Filters = {
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TQuickSearchSecretRotationsV2 = {
|
||||||
|
projectId: string;
|
||||||
|
folderMappings: { folderId: string; path: string; environment: string }[];
|
||||||
|
filters: TQuickSearchSecretRotationsV2Filters;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretRotationRotateGeneratedCredentials = {
|
||||||
|
auditLogInfo?: AuditLogInfo;
|
||||||
|
jobId?: string;
|
||||||
|
shouldSendNotification?: boolean;
|
||||||
|
isFinalAttempt?: boolean;
|
||||||
|
isManualRotation?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretRotationRotateSecretsJobPayload = { rotationId: string; queuedAt: Date; isManualRotation: boolean };
|
||||||
|
|
||||||
|
export type TSecretRotationSendNotificationJobPayload = {
|
||||||
|
secretRotation: TSecretRotationV2Raw;
|
||||||
|
};
|
||||||
|
|
||||||
|
// scott: the reason for the callback structure of the rotation factory is to facilitate, when possible,
|
||||||
|
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
|
||||||
|
// third party credential changes (when supported), preventing credentials getting out of sync
|
||||||
|
|
||||||
|
export type TRotationFactoryIssueCredentials = (
|
||||||
|
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw>
|
||||||
|
) => Promise<TSecretRotationV2Raw>;
|
||||||
|
|
||||||
|
export type TRotationFactoryRevokeCredentials = (
|
||||||
|
generatedCredentials: TSecretRotationV2GeneratedCredentials,
|
||||||
|
callback: () => Promise<TSecretRotationV2Raw>
|
||||||
|
) => Promise<TSecretRotationV2Raw>;
|
||||||
|
|
||||||
|
export type TRotationFactoryRotateCredentials = (
|
||||||
|
credentialsToRevoke: TSecretRotationV2GeneratedCredentials[number] | undefined,
|
||||||
|
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw>
|
||||||
|
) => Promise<TSecretRotationV2Raw>;
|
||||||
|
|
||||||
|
export type TRotationFactoryGetSecretsPayload = (
|
||||||
|
generatedCredentials: TSecretRotationV2GeneratedCredentials[number]
|
||||||
|
) => { key: string; value: string }[];
|
||||||
|
|
||||||
|
export type TRotationFactory = (secretRotation: TSecretRotationV2WithConnection) => {
|
||||||
|
issueCredentials: TRotationFactoryIssueCredentials;
|
||||||
|
revokeCredentials: TRotationFactoryRevokeCredentials;
|
||||||
|
rotateCredentials: TRotationFactoryRotateCredentials;
|
||||||
|
getSecretsPayload: TRotationFactoryGetSecretsPayload;
|
||||||
|
};
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
|
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
|
|
||||||
|
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||||
|
PostgresCredentialsRotationSchema,
|
||||||
|
MsSqlCredentialsRotationSchema
|
||||||
|
]);
|
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./sql-credentials-rotation-fns";
|
||||||
|
export * from "./sql-credentials-rotation-schemas";
|
@@ -0,0 +1,232 @@
|
|||||||
|
import { randomInt } from "crypto";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TRotationFactoryGetSecretsPayload,
|
||||||
|
TRotationFactoryIssueCredentials,
|
||||||
|
TRotationFactoryRevokeCredentials,
|
||||||
|
TRotationFactoryRotateCredentials
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TSqlCredentialsRotationGeneratedCredentials,
|
||||||
|
TSqlCredentialsRotationWithConnection
|
||||||
|
} from "./sql-credentials-rotation-types";
|
||||||
|
|
||||||
|
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||||
|
length: 48,
|
||||||
|
required: {
|
||||||
|
lowercase: 1,
|
||||||
|
uppercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
symbols: 0
|
||||||
|
},
|
||||||
|
allowedSymbols: "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
try {
|
||||||
|
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
||||||
|
|
||||||
|
const chars = {
|
||||||
|
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
digits: "0123456789",
|
||||||
|
symbols: allowedSymbols || "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (required.lowercase > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.lowercase)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.uppercase > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.uppercase)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.digits > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.digits)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.symbols > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.symbols)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||||
|
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||||
|
|
||||||
|
const allowedChars = Object.entries(chars)
|
||||||
|
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||||
|
.map(([, value]) => value)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
parts.push(
|
||||||
|
...Array(remainingLength)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||||
|
);
|
||||||
|
|
||||||
|
// shuffle the array to mix up the characters
|
||||||
|
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||||
|
const j = randomInt(i + 1);
|
||||||
|
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("");
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
throw new Error(`Failed to generate password: ${message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => {
|
||||||
|
const error = e as Error;
|
||||||
|
|
||||||
|
if (!error?.message) return "Unknown error";
|
||||||
|
|
||||||
|
let redactedMessage = error.message;
|
||||||
|
|
||||||
|
credentials.forEach(({ password }) => {
|
||||||
|
redactedMessage = redactedMessage.replaceAll(password, "*******************");
|
||||||
|
});
|
||||||
|
|
||||||
|
return redactedMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRotationWithConnection) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
parameters: { username1, username2 },
|
||||||
|
activeIndex,
|
||||||
|
secretsMapping
|
||||||
|
} = secretRotation;
|
||||||
|
|
||||||
|
const validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
||||||
|
const client = await getSqlConnectionClient({
|
||||||
|
...connection,
|
||||||
|
credentials: {
|
||||||
|
...connection.credentials,
|
||||||
|
...credentials
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.raw("SELECT 1");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(redactPasswords(error, [credentials]));
|
||||||
|
} finally {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueCredentials: TRotationFactoryIssueCredentials = async (callback) => {
|
||||||
|
const client = await getSqlConnectionClient(connection);
|
||||||
|
|
||||||
|
// For SQL, since we get existing users, we change both their passwords
|
||||||
|
// on issue to invalidate their existing passwords
|
||||||
|
const credentialsSet = [
|
||||||
|
{ username: username1, password: generatePassword() },
|
||||||
|
{ username: username2, password: generatePassword() }
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.transaction(async (tx) => {
|
||||||
|
for await (const credentials of credentialsSet) {
|
||||||
|
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(redactPasswords(error, credentialsSet));
|
||||||
|
} finally {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const credentials of credentialsSet) {
|
||||||
|
await validateCredentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(credentialsSet[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const revokeCredentials: TRotationFactoryRevokeCredentials = async (credentialsToRevoke, callback) => {
|
||||||
|
const client = await getSqlConnectionClient(connection);
|
||||||
|
|
||||||
|
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.transaction(async (tx) => {
|
||||||
|
for await (const credentials of revokedCredentials) {
|
||||||
|
// invalidate previous passwords
|
||||||
|
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(redactPasswords(error, revokedCredentials));
|
||||||
|
} finally {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotateCredentials: TRotationFactoryRotateCredentials = async (_, callback) => {
|
||||||
|
const client = await getSqlConnectionClient(connection);
|
||||||
|
|
||||||
|
// generate new password for the next active user
|
||||||
|
const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(redactPasswords(error, [credentials]));
|
||||||
|
} finally {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
await validateCredentials(credentials);
|
||||||
|
|
||||||
|
return callback(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsPayload: TRotationFactoryGetSecretsPayload = (generatedCredentials) => {
|
||||||
|
const { username, password } = secretsMapping;
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
{
|
||||||
|
key: username,
|
||||||
|
value: generatedCredentials.username
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: password,
|
||||||
|
value: generatedCredentials.password
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueCredentials,
|
||||||
|
revokeCredentials,
|
||||||
|
rotateCredentials,
|
||||||
|
getSecretsPayload,
|
||||||
|
validateCredentials
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretRotations } from "@app/lib/api-docs";
|
||||||
|
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||||
|
|
||||||
|
export const SqlCredentialsRotationGeneratedCredentialsSchema = z
|
||||||
|
.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.max(2);
|
||||||
|
|
||||||
|
export const SqlCredentialsRotationParametersSchema = z.object({
|
||||||
|
username1: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Username1 Required")
|
||||||
|
.describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.username1),
|
||||||
|
username2: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Username2 Required")
|
||||||
|
.describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.username2)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SqlCredentialsRotationSecretsMappingSchema = z.object({
|
||||||
|
username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.SQL_CREDENTIALS.username),
|
||||||
|
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.SQL_CREDENTIALS.password)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SqlCredentialsRotationTemplateSchema = z.object({
|
||||||
|
createUserStatement: z.string(),
|
||||||
|
secretsMapping: z.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string()
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,14 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TMsSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
|
import { TPostgresCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
|
|
||||||
|
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "./sql-credentials-rotation-schemas";
|
||||||
|
|
||||||
|
export type TSqlCredentialsRotationWithConnection =
|
||||||
|
| TPostgresCredentialsRotationWithConnection
|
||||||
|
| TMsSqlCredentialsRotationWithConnection;
|
||||||
|
|
||||||
|
export type TSqlCredentialsRotationGeneratedCredentials = z.infer<
|
||||||
|
typeof SqlCredentialsRotationGeneratedCredentialsSchema
|
||||||
|
>;
|
@@ -16,8 +16,8 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
|
|||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSecretRotationActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "../permission/project-permission";
|
} from "../permission/project-permission";
|
||||||
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
||||||
@@ -69,7 +69,10 @@ export const secretRotationServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretRotationActions.Read,
|
||||||
|
ProjectPermissionSub.SecretRotation
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
custom: [],
|
custom: [],
|
||||||
@@ -99,7 +102,7 @@ export const secretRotationServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionSecretRotationActions.Read,
|
||||||
ProjectPermissionSub.SecretRotation
|
ProjectPermissionSub.SecretRotation
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -208,7 +211,10 @@ export const secretRotationServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretRotationActions.Read,
|
||||||
|
ProjectPermissionSub.SecretRotation
|
||||||
|
);
|
||||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
const docs = await secretRotationDAL.findSecretV2({ projectId });
|
const docs = await secretRotationDAL.findSecretV2({ projectId });
|
||||||
@@ -254,7 +260,10 @@ export const secretRotationServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretRotationActions.Edit,
|
||||||
|
ProjectPermissionSub.SecretRotation
|
||||||
|
);
|
||||||
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
|
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
|
||||||
await secretRotationQueue.addToQueue(doc.id, doc.interval);
|
await secretRotationQueue.addToQueue(doc.id, doc.interval);
|
||||||
return doc;
|
return doc;
|
||||||
@@ -273,7 +282,7 @@ export const secretRotationServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionSecretRotationActions.Delete,
|
||||||
ProjectPermissionSub.SecretRotation
|
ProjectPermissionSub.SecretRotation
|
||||||
);
|
);
|
||||||
const deletedDoc = await secretRotationDAL.transaction(async (tx) => {
|
const deletedDoc = await secretRotationDAL.transaction(async (tx) => {
|
||||||
|
@@ -398,8 +398,32 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
if (shouldUseBridge) {
|
if (shouldUseBridge) {
|
||||||
const rollback = await snapshotDAL.transaction(async (tx) => {
|
const rollback = await snapshotDAL.transaction(async (tx) => {
|
||||||
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx);
|
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx);
|
||||||
// this will remove all secrets in current folder
|
const secretRotationIds = rollbackSnaps
|
||||||
const deletedTopLevelSecs = await secretV2BridgeDAL.delete({ folderId: snapshot.folderId }, tx);
|
.flatMap((snap) => snap.secretVersions)
|
||||||
|
.filter((el) => el.isRotatedSecret)
|
||||||
|
.map((el) => el.secretId);
|
||||||
|
|
||||||
|
// this will remove all secrets in current folder except rotated secrets which we ignore
|
||||||
|
const deletedTopLevelSecs = await secretV2BridgeDAL.delete(
|
||||||
|
{
|
||||||
|
$complex: {
|
||||||
|
operator: "and",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
operator: "eq",
|
||||||
|
field: "folderId",
|
||||||
|
value: snapshot.folderId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: "notIn",
|
||||||
|
field: "id",
|
||||||
|
value: secretRotationIds
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
|
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
|
||||||
// this will remove all secrets and folders on child
|
// this will remove all secrets and folders on child
|
||||||
// due to sql foreign key and link list connection removing the folders removes everything below too
|
// due to sql foreign key and link list connection removing the folders removes everything below too
|
||||||
@@ -424,28 +448,31 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
);
|
);
|
||||||
const secrets = await secretV2BridgeDAL.insertMany(
|
const secrets = await secretV2BridgeDAL.insertMany(
|
||||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||||
secretVersions.map(
|
secretVersions
|
||||||
({
|
.filter((v) => !v.isRotatedSecret)
|
||||||
latestSecretVersion,
|
.map(
|
||||||
version,
|
({
|
||||||
updatedAt,
|
latestSecretVersion,
|
||||||
createdAt,
|
version,
|
||||||
secretId,
|
updatedAt,
|
||||||
envId,
|
createdAt,
|
||||||
id,
|
secretId,
|
||||||
tags,
|
envId,
|
||||||
// exclude the bottom fields from the secret - they are for versioning only.
|
id,
|
||||||
userActorId,
|
tags,
|
||||||
identityActorId,
|
// exclude the bottom fields from the secret - they are for versioning only.
|
||||||
actorType,
|
userActorId,
|
||||||
...el
|
identityActorId,
|
||||||
}) => ({
|
actorType,
|
||||||
...el,
|
isRotatedSecret,
|
||||||
id: secretId,
|
...el
|
||||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
}) => ({
|
||||||
folderId
|
...el,
|
||||||
})
|
id: secretId,
|
||||||
)
|
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||||
|
folderId
|
||||||
|
})
|
||||||
|
)
|
||||||
),
|
),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@@ -181,6 +181,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SnapshotFolder}.folderVersionId`,
|
`${TableName.SnapshotFolder}.folderVersionId`,
|
||||||
`${TableName.SecretFolderVersion}.id`
|
`${TableName.SecretFolderVersion}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretRotationV2SecretMapping,
|
||||||
|
`${TableName.SecretRotationV2SecretMapping}.secretId`,
|
||||||
|
`${TableName.SecretVersionV2}.secretId`
|
||||||
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretVersionV2))
|
.select(selectAllTableCols(TableName.SecretVersionV2))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"),
|
db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"),
|
||||||
@@ -195,7 +200,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||||
|
db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)
|
||||||
);
|
);
|
||||||
return sqlNestRelationships({
|
return sqlNestRelationships({
|
||||||
data,
|
data,
|
||||||
@@ -221,7 +227,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
key: "id",
|
key: "id",
|
||||||
label: "secretVersions" as const,
|
label: "secretVersions" as const,
|
||||||
mapper: (el) => SecretVersionsV2Schema.parse(el),
|
mapper: (el) => ({
|
||||||
|
...SecretVersionsV2Schema.parse(el),
|
||||||
|
isRotatedSecret: Boolean(el.rotationId),
|
||||||
|
rotationId: el.rotationId
|
||||||
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "tagVersionId",
|
key: "tagVersionId",
|
||||||
@@ -476,6 +486,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||||
`${TableName.SecretTag}.id`
|
`${TableName.SecretTag}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretRotationV2SecretMapping,
|
||||||
|
`${TableName.SecretVersionV2}.secretId`,
|
||||||
|
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||||
|
)
|
||||||
.leftJoin<{ latestSecretVersion: number }>(
|
.leftJoin<{ latestSecretVersion: number }>(
|
||||||
(tx || db)(TableName.SecretVersionV2)
|
(tx || db)(TableName.SecretVersionV2)
|
||||||
.groupBy("secretId")
|
.groupBy("secretId")
|
||||||
@@ -506,7 +521,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||||
|
db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)
|
||||||
);
|
);
|
||||||
|
|
||||||
const formated = sqlNestRelationships({
|
const formated = sqlNestRelationships({
|
||||||
@@ -523,7 +539,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
|||||||
label: "secretVersions" as const,
|
label: "secretVersions" as const,
|
||||||
mapper: (el) => ({
|
mapper: (el) => ({
|
||||||
...SecretVersionsV2Schema.parse(el),
|
...SecretVersionsV2Schema.parse(el),
|
||||||
latestSecretVersion: el.latestSecretVersion as number
|
latestSecretVersion: el.latestSecretVersion as number,
|
||||||
|
isRotatedSecret: Boolean(el.rotationId)
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
export enum SshCertKeyAlgorithm {
|
||||||
|
RSA_2048 = "RSA_2048",
|
||||||
|
RSA_4096 = "RSA_4096",
|
||||||
|
ECDSA_P256 = "EC_prime256v1",
|
||||||
|
ECDSA_P384 = "EC_secp384r1",
|
||||||
|
ED25519 = "ED25519"
|
||||||
|
}
|
193
backend/src/ee/services/ssh-host/ssh-host-dal.ts
Normal file
193
backend/src/ee/services/ssh-host/ssh-host-dal.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
|
||||||
|
|
||||||
|
export const sshHostDALFactory = (db: TDbClient) => {
|
||||||
|
const sshHostOrm = ormify(db, TableName.SshHost);
|
||||||
|
|
||||||
|
const findUserAccessibleSshHosts = async (projectIds: string[], userId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const user = await (tx || db.replicaNode())(TableName.Users).where({ id: userId }).select("username").first();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new DatabaseError({ name: `${TableName.Users}: UserNotFound`, error: new Error("User not found") });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||||
|
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`)
|
||||||
|
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||||
|
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
|
db.ref("projectId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostname").withSchema(TableName.SshHost),
|
||||||
|
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||||
|
|
||||||
|
const grouped = groupBy(rows, (r) => r.sshHostId);
|
||||||
|
return Object.values(grouped).map((hostRows) => {
|
||||||
|
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0];
|
||||||
|
|
||||||
|
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
|
||||||
|
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: [user.username]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: sshHostId,
|
||||||
|
hostname,
|
||||||
|
projectId,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithPrincipalsAcrossProjects` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSshHostsWithLoginMappings = async (projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||||
|
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.where(`${TableName.SshHost}.projectId`, projectId)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
|
db.ref("projectId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostname").withSchema(TableName.SshHost),
|
||||||
|
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||||
|
|
||||||
|
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
||||||
|
return Object.values(hostsGrouped).map((hostRows) => {
|
||||||
|
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
||||||
|
|
||||||
|
const loginMappingGrouped = groupBy(
|
||||||
|
hostRows.filter((r) => r.loginUser),
|
||||||
|
(r) => r.loginUser
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: sshHostId,
|
||||||
|
hostname,
|
||||||
|
projectId,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithLoginMappings` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSshHostByIdWithLoginMappings = async (sshHostId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||||
|
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.where(`${TableName.SshHost}.id`, sshHostId)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
|
db.ref("projectId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostname").withSchema(TableName.SshHost),
|
||||||
|
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
|
||||||
|
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
||||||
|
|
||||||
|
const loginMappingGrouped = groupBy(
|
||||||
|
rows.filter((r) => r.loginUser),
|
||||||
|
(r) => r.loginUser
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
projectId,
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostByIdWithLoginMappings` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sshHostOrm,
|
||||||
|
findSshHostsWithLoginMappings,
|
||||||
|
findUserAccessibleSshHosts,
|
||||||
|
findSshHostByIdWithLoginMappings
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshHostLoginUserMappingDALFactory = ReturnType<typeof sshHostLoginUserMappingDALFactory>;
|
||||||
|
|
||||||
|
export const sshHostLoginUserMappingDALFactory = (db: TDbClient) => {
|
||||||
|
const sshHostLoginUserMappingOrm = ormify(db, TableName.SshHostLoginUserMapping);
|
||||||
|
return sshHostLoginUserMappingOrm;
|
||||||
|
};
|
20
backend/src/ee/services/ssh-host/ssh-host-schema.ts
Normal file
20
backend/src/ee/services/ssh-host/ssh-host-schema.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SshHostsSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshHost = SshHostsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
projectId: true,
|
||||||
|
hostname: true,
|
||||||
|
userCertTtl: true,
|
||||||
|
hostCertTtl: true,
|
||||||
|
userSshCaId: true,
|
||||||
|
hostSshCaId: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loginMappingSchema = z.object({
|
||||||
|
loginUser: z.string().trim(),
|
||||||
|
allowedPrincipals: z.object({
|
||||||
|
usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames)))
|
||||||
|
})
|
||||||
|
});
|
694
backend/src/ee/services/ssh-host/ssh-host-service.ts
Normal file
694
backend/src/ee/services/ssh-host/ssh-host-service.ts
Normal file
@@ -0,0 +1,694 @@
|
|||||||
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
|
||||||
|
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
|
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||||
|
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||||
|
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||||
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import {
|
||||||
|
convertActorToPrincipals,
|
||||||
|
createSshCert,
|
||||||
|
createSshKeyPair,
|
||||||
|
getSshPublicKey
|
||||||
|
} from "../ssh/ssh-certificate-authority-fns";
|
||||||
|
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||||
|
import {
|
||||||
|
TCreateSshHostDTO,
|
||||||
|
TDeleteSshHostDTO,
|
||||||
|
TGetSshHostDTO,
|
||||||
|
TIssueSshHostHostCertDTO,
|
||||||
|
TIssueSshHostUserCertDTO,
|
||||||
|
TListSshHostsDTO,
|
||||||
|
TUpdateSshHostDTO
|
||||||
|
} from "./ssh-host-types";
|
||||||
|
|
||||||
|
type TSshHostServiceFactoryDep = {
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById" | "find">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findOne">;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
||||||
|
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
||||||
|
sshHostDAL: Pick<
|
||||||
|
TSshHostDALFactory,
|
||||||
|
| "transaction"
|
||||||
|
| "create"
|
||||||
|
| "findById"
|
||||||
|
| "updateById"
|
||||||
|
| "deleteById"
|
||||||
|
| "findOne"
|
||||||
|
| "findSshHostByIdWithLoginMappings"
|
||||||
|
| "findUserAccessibleSshHosts"
|
||||||
|
>;
|
||||||
|
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
||||||
|
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
|
||||||
|
|
||||||
|
export const sshHostServiceFactory = ({
|
||||||
|
userDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateBodyDAL,
|
||||||
|
sshHostDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService
|
||||||
|
}: TSshHostServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Return list of all SSH hosts that a user can issue user SSH certificates for
|
||||||
|
* (i.e. is able to access / connect to) across all SSH projects in the organization
|
||||||
|
*/
|
||||||
|
const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => {
|
||||||
|
if (actor !== ActorType.USER) {
|
||||||
|
// (dangtony98): only support user for now
|
||||||
|
throw new BadRequestError({ message: `Actor type ${actor} not supported` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshProjects = await projectDAL.find({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
type: ProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
const allowedHosts = [];
|
||||||
|
|
||||||
|
for await (const project of sshProjects) {
|
||||||
|
try {
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectHosts = await sshHostDAL.findUserAccessibleSshHosts([project.id], actorId);
|
||||||
|
|
||||||
|
allowedHosts.push(...projectHosts);
|
||||||
|
} catch {
|
||||||
|
// intentionally ignore projects where user lacks access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedHosts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSshHost = async ({
|
||||||
|
projectId,
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
userSshCaId: requestedUserSshCaId,
|
||||||
|
hostSshCaId: requestedHostSshCaId,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshHostDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSshHostActions.Create,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolveSshCaId = async ({
|
||||||
|
requestedId,
|
||||||
|
fallbackId,
|
||||||
|
label
|
||||||
|
}: {
|
||||||
|
requestedId?: string;
|
||||||
|
fallbackId?: string | null;
|
||||||
|
label: "User" | "Host";
|
||||||
|
}) => {
|
||||||
|
const finalId = requestedId ?? fallbackId;
|
||||||
|
if (!finalId) {
|
||||||
|
throw new BadRequestError({ message: `Missing ${label.toLowerCase()} SSH CA` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findOne({
|
||||||
|
id: finalId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ca) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `${label} SSH CA with ID '${finalId}' not found in project '${projectId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ca.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectSshConfig = await projectSshConfigDAL.findOne({ projectId });
|
||||||
|
|
||||||
|
const userSshCaId = await resolveSshCaId({
|
||||||
|
requestedId: requestedUserSshCaId,
|
||||||
|
fallbackId: projectSshConfig?.defaultUserSshCaId,
|
||||||
|
label: "User"
|
||||||
|
});
|
||||||
|
|
||||||
|
const hostSshCaId = await resolveSshCaId({
|
||||||
|
requestedId: requestedHostSshCaId,
|
||||||
|
fallbackId: projectSshConfig?.defaultHostSshCaId,
|
||||||
|
label: "Host"
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSshHost = await sshHostDAL.transaction(async (tx) => {
|
||||||
|
const host = await sshHostDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// (dangtony98): room to optimize
|
||||||
|
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||||
|
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||||
|
{
|
||||||
|
sshHostId: host.id,
|
||||||
|
loginUser
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowedPrincipals.usernames.length > 0) {
|
||||||
|
const users = await userDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
username: allowedPrincipals.usernames
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundUsernames = new Set(users.map((u) => u.username));
|
||||||
|
|
||||||
|
for (const uname of allowedPrincipals.usernames) {
|
||||||
|
if (!foundUsernames.has(uname)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid username: ${uname}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const user of users) {
|
||||||
|
// check that each user has access to the SSH project
|
||||||
|
await permissionService.getUserProjectPermission({
|
||||||
|
userId: user.id,
|
||||||
|
projectId,
|
||||||
|
authMethod: actorAuthMethod,
|
||||||
|
userOrgId: actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sshHostLoginUserMappingDAL.insertMany(
|
||||||
|
users.map((user) => ({
|
||||||
|
sshHostLoginUserId: sshHostLoginUser.id,
|
||||||
|
userId: user.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
|
||||||
|
if (!newSshHostWithLoginMappings) {
|
||||||
|
throw new NotFoundError({ message: `SSH host with ID '${host.id}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSshHostWithLoginMappings;
|
||||||
|
});
|
||||||
|
|
||||||
|
return newSshHost;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSshHost = async ({
|
||||||
|
sshHostId,
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateSshHostDTO) => {
|
||||||
|
const host = await sshHostDAL.findById(sshHostId);
|
||||||
|
if (!host) throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: host.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSshHostActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedHost = await sshHostDAL.transaction(async (tx) => {
|
||||||
|
await sshHostDAL.updateById(
|
||||||
|
sshHostId,
|
||||||
|
{
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loginMappings) {
|
||||||
|
await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx);
|
||||||
|
if (loginMappings.length) {
|
||||||
|
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||||
|
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||||
|
{
|
||||||
|
sshHostId: host.id,
|
||||||
|
loginUser
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowedPrincipals.usernames.length > 0) {
|
||||||
|
const users = await userDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
username: allowedPrincipals.usernames
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundUsernames = new Set(users.map((u) => u.username));
|
||||||
|
|
||||||
|
for (const uname of allowedPrincipals.usernames) {
|
||||||
|
if (!foundUsernames.has(uname)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid username: ${uname}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const user of users) {
|
||||||
|
await permissionService.getUserProjectPermission({
|
||||||
|
userId: user.id,
|
||||||
|
projectId: host.projectId,
|
||||||
|
authMethod: actorAuthMethod,
|
||||||
|
userOrgId: actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sshHostLoginUserMappingDAL.insertMany(
|
||||||
|
users.map((user) => ({
|
||||||
|
sshHostLoginUserId: sshHostLoginUser.id,
|
||||||
|
userId: user.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId, tx);
|
||||||
|
if (!updatedHostWithLoginMappings) {
|
||||||
|
throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedHostWithLoginMappings;
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedHost;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteSshHostDTO) => {
|
||||||
|
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||||
|
if (!host) throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: host.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSshHostActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshHostDAL.deleteById(sshHostId);
|
||||||
|
|
||||||
|
return host;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => {
|
||||||
|
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||||
|
if (!host) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${sshHostId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: host.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSshHostActions.Read,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return host;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH certificate and corresponding new SSH public-private key pair where
|
||||||
|
* SSH public key is signed using CA behind SSH certificate with name [templateName].
|
||||||
|
*
|
||||||
|
* Note: Used for issuing SSH credentials as part of request against a specific SSH Host.
|
||||||
|
*/
|
||||||
|
const issueSshHostUserCert = async ({
|
||||||
|
sshHostId,
|
||||||
|
loginUser,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueSshHostUserCertDTO) => {
|
||||||
|
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||||
|
if (!host) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${sshHostId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: host.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
const internalPrincipals = await convertActorToPrincipals({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
userDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapping = host.loginMappings.find(
|
||||||
|
(m) =>
|
||||||
|
m.loginUser === loginUser &&
|
||||||
|
m.allowedPrincipals.usernames.some((allowed) => internalPrincipals.includes(allowed))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mapping) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `You are not allowed to login as ${loginUser} on this host`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyId = `${actor}-${actorId}`;
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: host.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// (dangtony98): will support more algorithms in the future
|
||||||
|
const keyAlgorithm = SshCertKeyAlgorithm.ED25519;
|
||||||
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||||
|
|
||||||
|
// (dangtony98): include the loginUser as a principal on the issued certificate
|
||||||
|
const principals = [...internalPrincipals, loginUser];
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
clientPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl: host.userCertTtl,
|
||||||
|
certType: SshCertType.USER
|
||||||
|
});
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: host.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedCertificate = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(signedPublicKey, "utf8")
|
||||||
|
}).cipherTextBlob;
|
||||||
|
|
||||||
|
await sshCertificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await sshCertificateDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: host.userSshCaId,
|
||||||
|
sshHostId: host.id,
|
||||||
|
serialNumber,
|
||||||
|
certType: SshCertType.USER,
|
||||||
|
principals,
|
||||||
|
keyId,
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(Date.now() + ttl * 1000)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
sshCertId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
principals,
|
||||||
|
serialNumber,
|
||||||
|
signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
ttl,
|
||||||
|
keyAlgorithm
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueSshHostHostCert = async ({
|
||||||
|
sshHostId,
|
||||||
|
publicKey,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueSshHostHostCertDTO) => {
|
||||||
|
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||||
|
if (!host) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${sshHostId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: host.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSshHostActions.IssueHostCert,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: host.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const principals = [host.hostname];
|
||||||
|
const keyId = `host-${host.id}`;
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
clientPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl: host.hostCertTtl,
|
||||||
|
certType: SshCertType.HOST
|
||||||
|
});
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: host.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedCertificate = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(signedPublicKey, "utf8")
|
||||||
|
}).cipherTextBlob;
|
||||||
|
|
||||||
|
await sshCertificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await sshCertificateDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: host.hostSshCaId,
|
||||||
|
sshHostId: host.id,
|
||||||
|
serialNumber,
|
||||||
|
certType: SshCertType.HOST,
|
||||||
|
principals,
|
||||||
|
keyId,
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(Date.now() + ttl * 1000)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
sshCertId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { host, principals, serialNumber, signedPublicKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshHostUserCaPk = async (sshHostId: string) => {
|
||||||
|
const host = await sshHostDAL.findById(sshHostId);
|
||||||
|
if (!host) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${sshHostId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: host.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshHostHostCaPk = async (sshHostId: string) => {
|
||||||
|
const host = await sshHostDAL.findById(sshHostId);
|
||||||
|
if (!host) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${sshHostId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: host.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listSshHosts,
|
||||||
|
createSshHost,
|
||||||
|
updateSshHost,
|
||||||
|
deleteSshHost,
|
||||||
|
getSshHost,
|
||||||
|
issueSshHostUserCert,
|
||||||
|
issueSshHostHostCert,
|
||||||
|
getSshHostUserCaPk,
|
||||||
|
getSshHostHostCaPk
|
||||||
|
};
|
||||||
|
};
|
48
backend/src/ee/services/ssh-host/ssh-host-types.ts
Normal file
48
backend/src/ee/services/ssh-host/ssh-host-types.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TCreateSshHostDTO = {
|
||||||
|
hostname: string;
|
||||||
|
userCertTtl: string;
|
||||||
|
hostCertTtl: string;
|
||||||
|
loginMappings: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
userSshCaId?: string;
|
||||||
|
hostSshCaId?: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateSshHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname?: string;
|
||||||
|
userCertTtl?: string;
|
||||||
|
hostCertTtl?: string;
|
||||||
|
loginMappings?: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteSshHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueSshHostUserCertDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
loginUser: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueSshHostHostCertDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
publicKey: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
15
backend/src/ee/services/ssh-host/ssh-host-validators.ts
Normal file
15
backend/src/ee/services/ssh-host/ssh-host-validators.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
|
|
||||||
|
export const isValidHostname = (value: string): boolean => {
|
||||||
|
if (typeof value !== "string") return false;
|
||||||
|
if (value.length > 255) return false;
|
||||||
|
|
||||||
|
// Only allow strict FQDNs, no wildcards or IPs
|
||||||
|
return isFQDN(value, {
|
||||||
|
require_tld: true,
|
||||||
|
allow_underscores: false,
|
||||||
|
allow_trailing_dot: false,
|
||||||
|
allow_numeric_tld: true,
|
||||||
|
allow_wildcard: false
|
||||||
|
});
|
||||||
|
};
|
10
backend/src/ee/services/ssh-host/ssh-login-user-dal.ts
Normal file
10
backend/src/ee/services/ssh-host/ssh-login-user-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshHostLoginUserDALFactory = ReturnType<typeof sshHostLoginUserDALFactory>;
|
||||||
|
|
||||||
|
export const sshHostLoginUserDALFactory = (db: TDbClient) => {
|
||||||
|
const sshHostLoginUserOrm = ormify(db, TableName.SshHostLoginUser);
|
||||||
|
return sshHostLoginUserOrm;
|
||||||
|
};
|
@@ -1,21 +1,31 @@
|
|||||||
import { execFile } from "child_process";
|
import { execFile } from "child_process";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
|
import { Knex } from "knex";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isValidHostPattern,
|
isValidHostPattern,
|
||||||
isValidUserPattern
|
isValidUserPattern
|
||||||
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
|
import {
|
||||||
|
SshCaKeySource,
|
||||||
|
SshCaStatus,
|
||||||
|
SshCertType,
|
||||||
|
TConvertActorToPrincipalsDTO,
|
||||||
|
TCreateSshCaHelperDTO,
|
||||||
|
TCreateSshCertDTO
|
||||||
|
} from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -31,31 +41,35 @@ export const createSshCertSerialNumber = () => {
|
|||||||
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
||||||
* We use this function because the key format generated by `ssh-keygen` is unique.
|
* We use this function because the key format generated by `ssh-keygen` is unique.
|
||||||
*/
|
*/
|
||||||
export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
export const createSshKeyPair = async (keyAlgorithm: SshCertKeyAlgorithm) => {
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
||||||
const privateKeyFile = path.join(tempDir, "id_key");
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
const publicKeyFile = `${privateKeyFile}.pub`;
|
const publicKeyFile = `${privateKeyFile}.pub`;
|
||||||
|
|
||||||
let keyType: string;
|
let keyType: string;
|
||||||
let keyBits: string;
|
let keyBits: string | null;
|
||||||
|
|
||||||
switch (keyAlgorithm) {
|
switch (keyAlgorithm) {
|
||||||
case CertKeyAlgorithm.RSA_2048:
|
case SshCertKeyAlgorithm.RSA_2048:
|
||||||
keyType = "rsa";
|
keyType = "rsa";
|
||||||
keyBits = "2048";
|
keyBits = "2048";
|
||||||
break;
|
break;
|
||||||
case CertKeyAlgorithm.RSA_4096:
|
case SshCertKeyAlgorithm.RSA_4096:
|
||||||
keyType = "rsa";
|
keyType = "rsa";
|
||||||
keyBits = "4096";
|
keyBits = "4096";
|
||||||
break;
|
break;
|
||||||
case CertKeyAlgorithm.ECDSA_P256:
|
case SshCertKeyAlgorithm.ECDSA_P256:
|
||||||
keyType = "ecdsa";
|
keyType = "ecdsa";
|
||||||
keyBits = "256";
|
keyBits = "256";
|
||||||
break;
|
break;
|
||||||
case CertKeyAlgorithm.ECDSA_P384:
|
case SshCertKeyAlgorithm.ECDSA_P384:
|
||||||
keyType = "ecdsa";
|
keyType = "ecdsa";
|
||||||
keyBits = "384";
|
keyBits = "384";
|
||||||
break;
|
break;
|
||||||
|
case SshCertKeyAlgorithm.ED25519:
|
||||||
|
keyType = "ed25519";
|
||||||
|
keyBits = null;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
|
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
|
||||||
@@ -63,10 +77,16 @@ export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const args = ["-t", keyType];
|
||||||
|
if (keyBits !== null) {
|
||||||
|
args.push("-b", keyBits);
|
||||||
|
}
|
||||||
|
args.push("-f", privateKeyFile, "-N", "");
|
||||||
|
|
||||||
// Generate the SSH key pair
|
// Generate the SSH key pair
|
||||||
// The "-N ''" sets an empty passphrase
|
// The "-N ''" sets an empty passphrase
|
||||||
// The keys are created in the temporary directory
|
// The keys are created in the temporary directory
|
||||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""], {
|
await execFileAsync("ssh-keygen", args, {
|
||||||
timeout: EXEC_TIMEOUT_MS
|
timeout: EXEC_TIMEOUT_MS
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,7 +300,12 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
|||||||
* that it only contains alphanumeric characters with no spaces.
|
* that it only contains alphanumeric characters with no spaces.
|
||||||
*/
|
*/
|
||||||
export const validateSshCertificateKeyId = (keyId: string) => {
|
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||||
const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
|
const regex = characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Hyphen,
|
||||||
|
CharacterType.Colon,
|
||||||
|
CharacterType.Period
|
||||||
|
]);
|
||||||
if (!regex(keyId)) {
|
if (!regex(keyId)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message:
|
||||||
@@ -322,6 +347,96 @@ const validateSshPublicKey = async (publicKey: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getKeyAlgorithmFromFingerprintOutput = (output: string): SshCertKeyAlgorithm | undefined => {
|
||||||
|
const parts = output.trim().split(" ");
|
||||||
|
const bitsInt = parseInt(parts[0], 10);
|
||||||
|
const keyTypeRaw = parts.at(-1)?.replace(/[()]/g, ""); // remove surrounding parentheses
|
||||||
|
|
||||||
|
if (keyTypeRaw === "RSA") {
|
||||||
|
return bitsInt === 2048 ? SshCertKeyAlgorithm.RSA_2048 : SshCertKeyAlgorithm.RSA_4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyTypeRaw === "ECDSA") {
|
||||||
|
return bitsInt === 256 ? SshCertKeyAlgorithm.ECDSA_P256 : SshCertKeyAlgorithm.ECDSA_P384;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyTypeRaw === "ED25519") {
|
||||||
|
return SshCertKeyAlgorithm.ED25519;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeSshPrivateKey = (raw: string): string => {
|
||||||
|
return `${raw
|
||||||
|
.replace(/\r\n/g, "\n") // Windows CRLF → LF
|
||||||
|
.replace(/\r/g, "\n") // Old Mac CR → LF
|
||||||
|
.replace(/\\n/g, "\n") // Double-escaped \n
|
||||||
|
.trim()}\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the format of the SSH private key
|
||||||
|
*
|
||||||
|
* Returns the SSH public key corresponding to the private key
|
||||||
|
* and the key algorithm categorization.
|
||||||
|
*/
|
||||||
|
export const validateSshPrivateKey = async (privateKey: string) => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-privkey-"));
|
||||||
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(privateKeyFile, privateKey, {
|
||||||
|
encoding: "utf8",
|
||||||
|
mode: 0o600
|
||||||
|
});
|
||||||
|
|
||||||
|
// This will fail if the private key is malformed or unreadable
|
||||||
|
const { stdout: publicKey } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], {
|
||||||
|
timeout: EXEC_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
|
||||||
|
const { stdout: fingerprint } = await execFileAsync("ssh-keygen", ["-lf", privateKeyFile]);
|
||||||
|
const keyAlgorithm = getKeyAlgorithmFromFingerprintOutput(fingerprint);
|
||||||
|
|
||||||
|
if (!keyAlgorithm) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH private key format: The key algorithm is not supported."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey,
|
||||||
|
keyAlgorithm
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH private key format: could not be parsed."
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the provided public and private keys are valid and constitute
|
||||||
|
* a matching SSH key pair.
|
||||||
|
*/
|
||||||
|
export const validateExternalSshCaKeyPair = async (publicKey: string, privateKey: string) => {
|
||||||
|
await validateSshPublicKey(publicKey);
|
||||||
|
|
||||||
|
const { publicKey: derivedPublicKey, keyAlgorithm } = await validateSshPrivateKey(privateKey);
|
||||||
|
|
||||||
|
if (publicKey.trim() !== derivedPublicKey.trim()) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to validate matching SSH key pair: The provided public key does not match the public key derived from the private key."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyAlgorithm;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an SSH certificate for a user or host.
|
* Create an SSH certificate for a user or host.
|
||||||
*/
|
*/
|
||||||
@@ -331,17 +446,32 @@ export const createSshCert = async ({
|
|||||||
clientPublicKey,
|
clientPublicKey,
|
||||||
keyId,
|
keyId,
|
||||||
principals,
|
principals,
|
||||||
requestedTtl,
|
requestedTtl, // in ms lib format
|
||||||
certType
|
certType
|
||||||
}: TCreateSshCertDTO) => {
|
}: TCreateSshCertDTO) => {
|
||||||
// validate if the requested [certType] is allowed under the template configuration
|
let ttl: number | undefined;
|
||||||
validateSshCertificateType(template, certType);
|
|
||||||
|
|
||||||
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
if (!template && requestedTtl) {
|
||||||
validateSshCertificatePrincipals(certType, template, principals);
|
const parsedTtl = Math.ceil(ms(requestedTtl) / 1000);
|
||||||
|
if (parsedTtl > 0) ttl = parsedTtl;
|
||||||
|
}
|
||||||
|
|
||||||
// validate if the requested TTL is valid under the template configuration
|
if (template) {
|
||||||
const ttl = validateSshCertificateTtl(template, requestedTtl);
|
// validate if the requested [certType] is allowed under the template configuration
|
||||||
|
validateSshCertificateType(template, certType);
|
||||||
|
|
||||||
|
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
||||||
|
validateSshCertificatePrincipals(certType, template, principals);
|
||||||
|
|
||||||
|
// validate if the requested TTL is valid under the template configuration
|
||||||
|
ttl = validateSshCertificateTtl(template, requestedTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ttl) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create SSH certificate due to missing TTL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
validateSshCertificateKeyId(keyId);
|
validateSshCertificateKeyId(keyId);
|
||||||
await validateSshPublicKey(clientPublicKey);
|
await validateSshPublicKey(clientPublicKey);
|
||||||
@@ -388,3 +518,88 @@ export const createSshCert = async ({
|
|||||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createSshCaHelper = async ({
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
keyAlgorithm: requestedKeyAlgorithm,
|
||||||
|
keySource,
|
||||||
|
externalPk,
|
||||||
|
externalSk,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
kmsService,
|
||||||
|
tx: outerTx
|
||||||
|
}: TCreateSshCaHelperDTO) => {
|
||||||
|
// Function to handle the actual creation logic
|
||||||
|
const processCreation = async (tx: Knex) => {
|
||||||
|
let publicKey: string;
|
||||||
|
let privateKey: string;
|
||||||
|
let keyAlgorithm: SshCertKeyAlgorithm = requestedKeyAlgorithm;
|
||||||
|
if (keySource === SshCaKeySource.INTERNAL) {
|
||||||
|
// generate SSH CA key pair internally
|
||||||
|
({ publicKey, privateKey } = await createSshKeyPair(requestedKeyAlgorithm));
|
||||||
|
} else {
|
||||||
|
// use external SSH CA key pair
|
||||||
|
if (!externalPk || !externalSk) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Public and private keys are required when key source is external"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
publicKey = externalPk;
|
||||||
|
privateKey = externalSk;
|
||||||
|
keyAlgorithm = await validateExternalSshCaKeyPair(publicKey, privateKey);
|
||||||
|
}
|
||||||
|
const ca = await sshCertificateAuthorityDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
status: SshCaStatus.ACTIVE,
|
||||||
|
keyAlgorithm,
|
||||||
|
keySource
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey(
|
||||||
|
{
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await sshCertificateAuthoritySecretDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: ca.id,
|
||||||
|
encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return { ...ca, publicKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (outerTx) {
|
||||||
|
return processCreation(outerTx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sshCertificateAuthorityDAL.transaction(processCreation);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an actor to a list of principals to be included in an SSH certificate.
|
||||||
|
*
|
||||||
|
* (dangtony98): This function is only supported for user actors at the moment and returns
|
||||||
|
* only the email of the associated user. In the future, we will consider other
|
||||||
|
* actor types and attributes such as group membership slugs and/or metadata to be
|
||||||
|
* included in the list of principals.
|
||||||
|
*/
|
||||||
|
export const convertActorToPrincipals = async ({ userDAL, actor, actorId }: TConvertActorToPrincipalsDTO) => {
|
||||||
|
if (actor !== ActorType.USER) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to convert actor to principals due to unsupported actor type"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await userDAL.findById(actorId);
|
||||||
|
|
||||||
|
return [user.username];
|
||||||
|
};
|
||||||
|
@@ -5,5 +5,6 @@ export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({
|
|||||||
projectId: true,
|
projectId: true,
|
||||||
friendlyName: true,
|
friendlyName: true,
|
||||||
status: true,
|
status: true,
|
||||||
keyAlgorithm: true
|
keyAlgorithm: true,
|
||||||
|
keySource: true
|
||||||
});
|
});
|
||||||
|
@@ -13,7 +13,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
import { createSshCaHelper, createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
||||||
import {
|
import {
|
||||||
SshCaStatus,
|
SshCaStatus,
|
||||||
TCreateSshCaDTO,
|
TCreateSshCaDTO,
|
||||||
@@ -59,7 +59,10 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
const createSshCa = async ({
|
const createSshCa = async ({
|
||||||
projectId,
|
projectId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
keyAlgorithm,
|
keyAlgorithm: requestedKeyAlgorithm,
|
||||||
|
publicKey: externalPk,
|
||||||
|
privateKey: externalSk,
|
||||||
|
keySource,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
@@ -79,33 +82,16 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
ProjectPermissionSub.SshCertificateAuthorities
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
);
|
);
|
||||||
|
|
||||||
const newCa = await sshCertificateAuthorityDAL.transaction(async (tx) => {
|
const newCa = await createSshCaHelper({
|
||||||
const ca = await sshCertificateAuthorityDAL.create(
|
projectId,
|
||||||
{
|
friendlyName,
|
||||||
projectId,
|
keyAlgorithm: requestedKeyAlgorithm,
|
||||||
friendlyName,
|
keySource,
|
||||||
status: SshCaStatus.ACTIVE,
|
externalPk,
|
||||||
keyAlgorithm
|
externalSk,
|
||||||
},
|
sshCertificateAuthorityDAL,
|
||||||
tx
|
sshCertificateAuthoritySecretDAL,
|
||||||
);
|
kmsService
|
||||||
|
|
||||||
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
|
||||||
|
|
||||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
type: KmsDataKey.SecretManager,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
await sshCertificateAuthoritySecretDAL.create(
|
|
||||||
{
|
|
||||||
sshCaId: ca.id,
|
|
||||||
encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ...ca, publicKey };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return newCa;
|
return newCa;
|
||||||
|
@@ -1,12 +1,24 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
export enum SshCaStatus {
|
export enum SshCaStatus {
|
||||||
ACTIVE = "active",
|
ACTIVE = "active",
|
||||||
DISABLED = "disabled"
|
DISABLED = "disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SshCaKeySource {
|
||||||
|
INTERNAL = "internal",
|
||||||
|
EXTERNAL = "external"
|
||||||
|
}
|
||||||
|
|
||||||
export enum SshCertType {
|
export enum SshCertType {
|
||||||
USER = "user",
|
USER = "user",
|
||||||
HOST = "host"
|
HOST = "host"
|
||||||
@@ -14,9 +26,25 @@ export enum SshCertType {
|
|||||||
|
|
||||||
export type TCreateSshCaDTO = {
|
export type TCreateSshCaDTO = {
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
|
publicKey?: string;
|
||||||
|
privateKey?: string;
|
||||||
|
keySource: SshCaKeySource;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TCreateSshCaHelperDTO = {
|
||||||
|
projectId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
|
keySource: SshCaKeySource;
|
||||||
|
externalPk?: string;
|
||||||
|
externalSk?: string;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "transaction" | "create">;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
tx?: Knex;
|
||||||
|
};
|
||||||
|
|
||||||
export type TGetSshCaDTO = {
|
export type TGetSshCaDTO = {
|
||||||
caId: string;
|
caId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -37,7 +65,7 @@ export type TDeleteSshCaDTO = {
|
|||||||
|
|
||||||
export type TIssueSshCredsDTO = {
|
export type TIssueSshCredsDTO = {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
certType: SshCertType;
|
certType: SshCertType;
|
||||||
principals: string[];
|
principals: string[];
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
@@ -58,7 +86,7 @@ export type TGetSshCaCertificateTemplatesDTO = {
|
|||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TCreateSshCertDTO = {
|
export type TCreateSshCertDTO = {
|
||||||
template: TSshCertificateTemplates;
|
template?: TSshCertificateTemplates;
|
||||||
caPrivateKey: string;
|
caPrivateKey: string;
|
||||||
clientPublicKey: string;
|
clientPublicKey: string;
|
||||||
keyId: string;
|
keyId: string;
|
||||||
@@ -66,3 +94,9 @@ export type TCreateSshCertDTO = {
|
|||||||
requestedTtl?: string;
|
requestedTtl?: string;
|
||||||
certType: SshCertType;
|
certType: SshCertType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TConvertActorToPrincipalsDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
@@ -8,7 +8,8 @@ export const PgSqlLock = {
|
|||||||
SuperAdminInit: 2024,
|
SuperAdminInit: 2024,
|
||||||
KmsRootKeyInit: 2025,
|
KmsRootKeyInit: 2025,
|
||||||
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
||||||
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`)
|
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`),
|
||||||
|
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`)
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||||
@@ -33,6 +34,7 @@ export const KeyStorePrefixes = {
|
|||||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
||||||
|
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
|
||||||
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||||
`identity-access-token-status:${identityAccessTokenId}`,
|
`identity-access-token-status:${identityAccessTokenId}`,
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import {
|
||||||
|
SECRET_ROTATION_CONNECTION_MAP,
|
||||||
|
SECRET_ROTATION_NAME_MAP
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
@@ -61,6 +66,17 @@ export const IDENTITIES = {
|
|||||||
},
|
},
|
||||||
LIST: {
|
LIST: {
|
||||||
orgId: "The ID of the organization to list identities."
|
orgId: "The ID of the organization to list identities."
|
||||||
|
},
|
||||||
|
SEARCH: {
|
||||||
|
search: {
|
||||||
|
desc: "The filters to apply to the search.",
|
||||||
|
name: "The name of the identity to filter by.",
|
||||||
|
role: "The organizational role of the identity to filter by."
|
||||||
|
},
|
||||||
|
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
|
||||||
|
limit: "The number of identities to return.",
|
||||||
|
orderBy: "The column to order identities by.",
|
||||||
|
orderDirection: "The direction to order identities in."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -503,6 +519,9 @@ export const PROJECTS = {
|
|||||||
LIST_SSH_CAS: {
|
LIST_SSH_CAS: {
|
||||||
projectId: "The ID of the project to list SSH CAs for."
|
projectId: "The ID of the project to list SSH CAs for."
|
||||||
},
|
},
|
||||||
|
LIST_SSH_HOSTS: {
|
||||||
|
projectId: "The ID of the project to list SSH hosts for."
|
||||||
|
},
|
||||||
LIST_SSH_CERTIFICATES: {
|
LIST_SSH_CERTIFICATES: {
|
||||||
projectId: "The ID of the project to list SSH certificates for.",
|
projectId: "The ID of the project to list SSH certificates for.",
|
||||||
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
|
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
|
||||||
@@ -819,7 +838,8 @@ export const DASHBOARD = {
|
|||||||
includeSecrets: "Whether to include project secrets in the response.",
|
includeSecrets: "Whether to include project secrets in the response.",
|
||||||
includeFolders: "Whether to include project folders in the response.",
|
includeFolders: "Whether to include project folders in the response.",
|
||||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response.",
|
includeDynamicSecrets: "Whether to include dynamic project secrets in the response.",
|
||||||
includeImports: "Whether to include project secret imports in the response."
|
includeImports: "Whether to include project secret imports in the response.",
|
||||||
|
includeSecretRotations: "Whether to include project secret rotations in the response."
|
||||||
},
|
},
|
||||||
SECRET_DETAILS_LIST: {
|
SECRET_DETAILS_LIST: {
|
||||||
projectId: "The ID of the project to list secrets/folders from.",
|
projectId: "The ID of the project to list secrets/folders from.",
|
||||||
@@ -834,7 +854,8 @@ export const DASHBOARD = {
|
|||||||
includeSecrets: "Whether to include project secrets in the response.",
|
includeSecrets: "Whether to include project secrets in the response.",
|
||||||
includeFolders: "Whether to include project folders in the response.",
|
includeFolders: "Whether to include project folders in the response.",
|
||||||
includeImports: "Whether to include project secret imports in the response.",
|
includeImports: "Whether to include project secret imports in the response.",
|
||||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
includeDynamicSecrets: "Whether to include dynamic project secrets in the response.",
|
||||||
|
includeSecretRotations: "Whether to include secret rotations in the response."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -1235,7 +1256,11 @@ export const SSH_CERTIFICATE_AUTHORITIES = {
|
|||||||
CREATE: {
|
CREATE: {
|
||||||
projectId: "The ID of the project to create the SSH CA in.",
|
projectId: "The ID of the project to create the SSH CA in.",
|
||||||
friendlyName: "A friendly name for the SSH CA.",
|
friendlyName: "A friendly name for the SSH CA.",
|
||||||
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA."
|
keyAlgorithm:
|
||||||
|
"The type of public key algorithm and size, in bits, of the key pair for the SSH CA; required if keySource is internal.",
|
||||||
|
publicKey: "The public key for the SSH CA key pair; required if keySource is external.",
|
||||||
|
privateKey: "The private key for the SSH CA key pair; required if keySource is external.",
|
||||||
|
keySource: "The source of the SSH CA key pair. This can be one of internal or external."
|
||||||
},
|
},
|
||||||
GET: {
|
GET: {
|
||||||
sshCaId: "The ID of the SSH CA to get."
|
sshCaId: "The ID of the SSH CA to get."
|
||||||
@@ -1309,6 +1334,62 @@ export const SSH_CERTIFICATE_TEMPLATES = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SSH_HOSTS = {
|
||||||
|
GET: {
|
||||||
|
sshHostId: "The ID of the SSH host to get."
|
||||||
|
},
|
||||||
|
CREATE: {
|
||||||
|
projectId: "The ID of the project to create the SSH host in.",
|
||||||
|
hostname: "The hostname of the SSH host.",
|
||||||
|
userCertTtl: "The time to live for user certificates issued under this host.",
|
||||||
|
hostCertTtl: "The time to live for host certificates issued under this host.",
|
||||||
|
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||||
|
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||||
|
loginMappings:
|
||||||
|
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project.",
|
||||||
|
userSshCaId:
|
||||||
|
"The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.",
|
||||||
|
hostSshCaId:
|
||||||
|
"The ID of the SSH CA to use for host certificates. If not specified, the default host SSH CA will be used if it exists."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
sshHostId: "The ID of the SSH host to update.",
|
||||||
|
hostname: "The hostname of the SSH host to update to.",
|
||||||
|
userCertTtl: "The time to live for user certificates issued under this host to update to.",
|
||||||
|
hostCertTtl: "The time to live for host certificates issued under this host to update to.",
|
||||||
|
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||||
|
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||||
|
loginMappings:
|
||||||
|
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
sshHostId: "The ID of the SSH host to delete."
|
||||||
|
},
|
||||||
|
ISSUE_SSH_CREDENTIALS: {
|
||||||
|
sshHostId: "The ID of the SSH host to issue the SSH credentials for.",
|
||||||
|
loginUser: "The login user to issue the SSH credentials for.",
|
||||||
|
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH host.",
|
||||||
|
serialNumber: "The serial number of the issued SSH certificate.",
|
||||||
|
signedKey: "The SSH certificate or signed SSH public key.",
|
||||||
|
privateKey: "The private key corresponding to the issued SSH certificate.",
|
||||||
|
publicKey: "The public key of the issued SSH certificate."
|
||||||
|
},
|
||||||
|
ISSUE_HOST_CERT: {
|
||||||
|
sshHostId: "The ID of the SSH host to issue the SSH certificate for.",
|
||||||
|
publicKey: "The SSH public key to issue the SSH certificate for.",
|
||||||
|
serialNumber: "The serial number of the issued SSH certificate.",
|
||||||
|
signedKey: "The SSH certificate or signed SSH public key."
|
||||||
|
},
|
||||||
|
GET_USER_CA_PUBLIC_KEY: {
|
||||||
|
sshHostId: "The ID of the SSH host to get the user SSH CA public key for.",
|
||||||
|
publicKey: "The public key of the user SSH CA linked to the SSH host."
|
||||||
|
},
|
||||||
|
GET_HOST_CA_PUBLIC_KEY: {
|
||||||
|
sshHostId: "The ID of the SSH host to get the host SSH CA public key for.",
|
||||||
|
publicKey: "The public key of the host SSH CA linked to the SSH host."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES = {
|
export const CERTIFICATE_AUTHORITIES = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectSlug: "Slug of the project to create the CA in.",
|
projectSlug: "Slug of the project to create the CA in.",
|
||||||
@@ -1659,7 +1740,8 @@ export const AppConnections = {
|
|||||||
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||||
description: `An optional description for the ${appName} Connection.`,
|
description: `An optional description for the ${appName} Connection.`,
|
||||||
credentials: `The credentials used to connect with ${appName}.`,
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
method: `The method used to authenticate with ${appName}.`
|
method: `The method used to authenticate with ${appName}.`,
|
||||||
|
isPlatformManagedCredentials: `Whether or not the ${appName} Connection credentials should be managed by Infisical. Once enabled this cannot be reversed.`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
UPDATE: (app: AppConnection) => {
|
UPDATE: (app: AppConnection) => {
|
||||||
@@ -1669,12 +1751,35 @@ export const AppConnections = {
|
|||||||
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||||
description: `The updated description of the ${appName} Connection.`,
|
description: `The updated description of the ${appName} Connection.`,
|
||||||
credentials: `The credentials used to connect with ${appName}.`,
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
method: `The method used to authenticate with ${appName}.`
|
method: `The method used to authenticate with ${appName}.`,
|
||||||
|
isPlatformManagedCredentials: `Whether or not the ${appName} Connection credentials should be managed by Infisical. Once enabled this cannot be reversed.`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
DELETE: (app: AppConnection) => ({
|
DELETE: (app: AppConnection) => ({
|
||||||
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.`
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.`
|
||||||
})
|
}),
|
||||||
|
CREDENTIALS: {
|
||||||
|
SQL_CONNECTION: {
|
||||||
|
host: "The hostname of the database server.",
|
||||||
|
port: "The port number of the database.",
|
||||||
|
database: "The name of the database to connect to.",
|
||||||
|
username: "The username to connect to the database with.",
|
||||||
|
password: "The password to connect to the database with.",
|
||||||
|
sslEnabled: "Whether or not to use SSL when connecting to the database.",
|
||||||
|
sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.",
|
||||||
|
sslCertificate: "The SSL certificate to use for connection."
|
||||||
|
},
|
||||||
|
TERRAFORM_CLOUD: {
|
||||||
|
apiToken: "The API token to use to connect with Terraform Cloud."
|
||||||
|
},
|
||||||
|
VERCEL: {
|
||||||
|
apiToken: "The API token used to authenticate with Vercel."
|
||||||
|
},
|
||||||
|
CAMUNDA: {
|
||||||
|
clientId: "The client ID used to authenticate with Camunda.",
|
||||||
|
clientSecret: "The client secret used to authenticate with Camunda."
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretSyncs = {
|
export const SecretSyncs = {
|
||||||
@@ -1783,11 +1888,98 @@ export const SecretSyncs = {
|
|||||||
DATABRICKS: {
|
DATABRICKS: {
|
||||||
scope: "The Databricks secret scope that secrets should be synced to."
|
scope: "The Databricks secret scope that secrets should be synced to."
|
||||||
},
|
},
|
||||||
|
CAMUNDA: {
|
||||||
|
scope: "The Camunda scope that secrets should be synced to.",
|
||||||
|
clusterUUID: "The UUID of the Camunda cluster that secrets should be synced to."
|
||||||
|
},
|
||||||
HUMANITEC: {
|
HUMANITEC: {
|
||||||
app: "The ID of the Humanitec app to sync secrets to.",
|
app: "The ID of the Humanitec app to sync secrets to.",
|
||||||
org: "The ID of the Humanitec org to sync secrets to.",
|
org: "The ID of the Humanitec org to sync secrets to.",
|
||||||
env: "The ID of the Humanitec environment to sync secrets to.",
|
env: "The ID of the Humanitec environment to sync secrets to.",
|
||||||
scope: "The Humanitec scope that secrets should be synced to."
|
scope: "The Humanitec scope that secrets should be synced to."
|
||||||
|
},
|
||||||
|
TERRAFORM_CLOUD: {
|
||||||
|
org: "The ID of the Terraform Cloud org to sync secrets to.",
|
||||||
|
variableSetName: "The name of the Terraform Cloud Variable Set to sync secrets to.",
|
||||||
|
variableSetId: "The ID of the Terraform Cloud Variable Set to sync secrets to.",
|
||||||
|
workspaceName: "The name of the Terraform Cloud workspace to sync secrets to.",
|
||||||
|
workspaceId: "The ID of the Terraform Cloud workspace to sync secrets to.",
|
||||||
|
scope: "The Terraform Cloud scope that secrets should be synced to.",
|
||||||
|
category: "The Terraform Cloud category that secrets should be synced to."
|
||||||
|
},
|
||||||
|
VERCEL: {
|
||||||
|
app: "The ID of the Vercel app to sync secrets to.",
|
||||||
|
appName: "The name of the Vercel app to sync secrets to.",
|
||||||
|
env: "The ID of the Vercel environment to sync secrets to.",
|
||||||
|
branch: "The branch to sync preview secrets to.",
|
||||||
|
teamId: "The ID of the Vercel team to sync secrets to."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretRotations = {
|
||||||
|
LIST: (type?: SecretRotation) => ({
|
||||||
|
projectId: `The ID of the project to list ${type ? SECRET_ROTATION_NAME_MAP[type] : "Secret"} Rotations from.`
|
||||||
|
}),
|
||||||
|
GET_BY_ID: (type: SecretRotation) => ({
|
||||||
|
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`
|
||||||
|
}),
|
||||||
|
GET_GENERATED_CREDENTIALS_BY_ID: (type: SecretRotation) => ({
|
||||||
|
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve the generated credentials for.`
|
||||||
|
}),
|
||||||
|
GET_BY_NAME: (type: SecretRotation) => ({
|
||||||
|
rotationName: `The name of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`,
|
||||||
|
projectId: `The ID of the project the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is located in.`,
|
||||||
|
secretPath: `The secret path the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is located at.`,
|
||||||
|
environment: `The environment the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is located in.`
|
||||||
|
}),
|
||||||
|
CREATE: (type: SecretRotation) => {
|
||||||
|
const destinationName = SECRET_ROTATION_NAME_MAP[type];
|
||||||
|
return {
|
||||||
|
name: `The name of the ${destinationName} Rotation to create. Must be slug-friendly.`,
|
||||||
|
description: `An optional description for the ${destinationName} Rotation.`,
|
||||||
|
projectId: "The ID of the project to create the rotation in.",
|
||||||
|
environment: `The slug of the project environment to create the rotation in.`,
|
||||||
|
secretPath: `The secret path of the project to create the rotation in.`,
|
||||||
|
connectionId: `The ID of the ${
|
||||||
|
APP_CONNECTION_NAME_MAP[SECRET_ROTATION_CONNECTION_MAP[type]]
|
||||||
|
} Connection to use for rotation.`,
|
||||||
|
isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified rotation interval has elapsed.`,
|
||||||
|
rotationInterval: `The interval, in days, to automatically rotate secrets.`,
|
||||||
|
rotateAtUtc: `The hours and minutes rotation should occur at in UTC. Defaults to Midnight (00:00) UTC.`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
UPDATE: (type: SecretRotation) => {
|
||||||
|
const typeName = SECRET_ROTATION_NAME_MAP[type];
|
||||||
|
return {
|
||||||
|
rotationId: `The ID of the ${typeName} Rotation to be updated.`,
|
||||||
|
name: `The updated name of the ${typeName} Rotation. Must be slug-friendly.`,
|
||||||
|
description: `The updated description of the ${typeName} Rotation.`,
|
||||||
|
isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified rotation interval has elapsed.`,
|
||||||
|
rotationInterval: `The updated interval, in days, to automatically rotate secrets.`,
|
||||||
|
rotateAtUtc: `The updated hours and minutes rotation should occur at in UTC.`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
DELETE: (type: SecretRotation) => ({
|
||||||
|
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to be deleted.`,
|
||||||
|
deleteSecrets: `Whether the mapped secrets belonging to this rotation should be deleted.`,
|
||||||
|
revokeGeneratedCredentials: `Whether the generated credentials associated with this rotation should be revoked.`
|
||||||
|
}),
|
||||||
|
ROTATE: (type: SecretRotation) => ({
|
||||||
|
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to rotate generated credentials for.`
|
||||||
|
}),
|
||||||
|
PARAMETERS: {
|
||||||
|
SQL_CREDENTIALS: {
|
||||||
|
username1:
|
||||||
|
"The username of the first login to rotate passwords for. This user must already exists in your database.",
|
||||||
|
username2:
|
||||||
|
"The username of the second login to rotate passwords for. This user must already exists in your database."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SECRETS_MAPPING: {
|
||||||
|
SQL_CREDENTIALS: {
|
||||||
|
username: "The name of the secret that the active username will be mapped to.",
|
||||||
|
password: "The name of the secret that the generated password will be mapped to."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -58,6 +58,7 @@ const envSchema = z
|
|||||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||||
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
|
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
|
||||||
HTTPS_ENABLED: zodStrBool,
|
HTTPS_ENABLED: zodStrBool,
|
||||||
|
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
|
||||||
// smtp options
|
// smtp options
|
||||||
SMTP_HOST: zpStr(z.string().optional()),
|
SMTP_HOST: zpStr(z.string().optional()),
|
||||||
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||||
@@ -192,6 +193,7 @@ const envSchema = z
|
|||||||
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
|
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
|
||||||
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"),
|
||||||
/* ----------------------------------------------------------------------------- */
|
/* ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* App Connections ----------------------------------------------------------------------------- */
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
@@ -262,6 +264,7 @@ const envSchema = z
|
|||||||
isSmtpConfigured: Boolean(data.SMTP_HOST),
|
isSmtpConfigured: Boolean(data.SMTP_HOST),
|
||||||
isRedisConfigured: Boolean(data.REDIS_URL),
|
isRedisConfigured: Boolean(data.REDIS_URL),
|
||||||
isDevelopmentMode: data.NODE_ENV === "development",
|
isDevelopmentMode: data.NODE_ENV === "development",
|
||||||
|
isRotationDevelopmentMode: data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE,
|
||||||
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
|
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
|
||||||
|
|
||||||
isSecretScanningConfigured:
|
isSecretScanningConfigured:
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { URL } from "url"; // Import the URL class
|
import { URL } from "url"; // Import the URL class
|
||||||
|
|
||||||
export const getDbConnectionHost = (urlString: string) => {
|
export const getDbConnectionHost = (urlString?: string) => {
|
||||||
|
if (!urlString) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(urlString);
|
const url = new URL(urlString);
|
||||||
// Split hostname and port (if provided)
|
// Split hostname and port (if provided)
|
||||||
|
@@ -2,11 +2,17 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { UnauthorizedError } from "../errors";
|
import { UnauthorizedError } from "../errors";
|
||||||
|
|
||||||
type TKnexDynamicPrimitiveOperator<T extends object> = {
|
type TKnexDynamicPrimitiveOperator<T extends object> =
|
||||||
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
| {
|
||||||
value: string;
|
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
||||||
field: Extract<keyof T, string>;
|
value: string;
|
||||||
};
|
field: Extract<keyof T, string>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
operator: "notIn";
|
||||||
|
value: string[];
|
||||||
|
field: Extract<keyof T, string>;
|
||||||
|
};
|
||||||
|
|
||||||
type TKnexDynamicInOperator<T extends object> = {
|
type TKnexDynamicInOperator<T extends object> = {
|
||||||
operator: "in";
|
operator: "in";
|
||||||
@@ -48,6 +54,10 @@ export const buildDynamicKnexQuery = <T extends object>(
|
|||||||
void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`);
|
void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "notIn": {
|
||||||
|
void queryBuilder.whereNotIn(filterAst.field, filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "and": {
|
case "and": {
|
||||||
filterAst.value.forEach((el) => {
|
filterAst.value.forEach((el) => {
|
||||||
void queryBuilder.andWhere((subQueryBuilder) => {
|
void queryBuilder.andWhere((subQueryBuilder) => {
|
||||||
|
@@ -7,7 +7,7 @@ export const prependTableNameToFindFilter = (tableName: TableName, filterObj: ob
|
|||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(filterObj).map(([key, value]) =>
|
Object.entries(filterObj).map(([key, value]) =>
|
||||||
key.startsWith("$")
|
key.startsWith("$")
|
||||||
? [key, prependTableNameToFindFilter(tableName, value as object)]
|
? [key, value ? prependTableNameToFindFilter(tableName, value as object) : value]
|
||||||
: [`${tableName}.${key}`, value]
|
: [`${tableName}.${key}`, value]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
141
backend/src/lib/search-resource/db.ts
Normal file
141
backend/src/lib/search-resource/db.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { SearchResourceOperators, TSearchResourceOperator } from "./search";
|
||||||
|
|
||||||
|
const buildKnexQuery = (
|
||||||
|
query: Knex.QueryBuilder,
|
||||||
|
// when it's multiple table field means it's field1 or field2
|
||||||
|
fields: string | string[],
|
||||||
|
operator: SearchResourceOperators,
|
||||||
|
value: unknown
|
||||||
|
) => {
|
||||||
|
switch (operator) {
|
||||||
|
case SearchResourceOperators.$eq: {
|
||||||
|
if (typeof value !== "string" && typeof value !== "number")
|
||||||
|
throw new Error("Invalid value type for $eq operator");
|
||||||
|
|
||||||
|
if (typeof fields === "string") {
|
||||||
|
return void query.where(fields, "=", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return void query.where((qb) => {
|
||||||
|
return fields.forEach((el, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return void qb.where(el, "=", value);
|
||||||
|
}
|
||||||
|
return void qb.orWhere(el, "=", value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case SearchResourceOperators.$neq: {
|
||||||
|
if (typeof value !== "string" && typeof value !== "number")
|
||||||
|
throw new Error("Invalid value type for $neq operator");
|
||||||
|
|
||||||
|
if (typeof fields === "string") {
|
||||||
|
return void query.where(fields, "<>", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return void query.where((qb) => {
|
||||||
|
return fields.forEach((el, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return void qb.where(el, "<>", value);
|
||||||
|
}
|
||||||
|
return void qb.orWhere(el, "<>", value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case SearchResourceOperators.$in: {
|
||||||
|
if (!Array.isArray(value)) throw new Error("Invalid value type for $in operator");
|
||||||
|
|
||||||
|
if (typeof fields === "string") {
|
||||||
|
return void query.whereIn(fields, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return void query.where((qb) => {
|
||||||
|
return fields.forEach((el, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return void qb.whereIn(el, value);
|
||||||
|
}
|
||||||
|
return void qb.orWhereIn(el, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case SearchResourceOperators.$contains: {
|
||||||
|
if (typeof value !== "string") throw new Error("Invalid value type for $contains operator");
|
||||||
|
|
||||||
|
if (typeof fields === "string") {
|
||||||
|
return void query.whereILike(fields, `%${value}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return void query.where((qb) => {
|
||||||
|
return fields.forEach((el, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return void qb.whereILike(el, `%${value}%`);
|
||||||
|
}
|
||||||
|
return void qb.orWhereILike(el, `%${value}%`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported operator: ${String(operator)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildKnexFilterForSearchResource = <T extends { [K: string]: TSearchResourceOperator }, K extends keyof T>(
|
||||||
|
rootQuery: Knex.QueryBuilder,
|
||||||
|
searchFilter: T & { $or?: T[] },
|
||||||
|
getAttributeField: (attr: K) => string | string[] | null
|
||||||
|
) => {
|
||||||
|
const { $or: orFilters = [] } = searchFilter;
|
||||||
|
(Object.keys(searchFilter) as K[]).forEach((key) => {
|
||||||
|
// akhilmhdh: yes, we could have split in top. This is done to satisfy ts type error
|
||||||
|
if (key === "$or") return;
|
||||||
|
|
||||||
|
const dbField = getAttributeField(key);
|
||||||
|
if (!dbField) throw new Error(`DB field not found for ${String(key)}`);
|
||||||
|
|
||||||
|
const dbValue = searchFilter[key];
|
||||||
|
if (typeof dbValue === "string" || typeof dbValue === "number") {
|
||||||
|
buildKnexQuery(rootQuery, dbField, SearchResourceOperators.$eq, dbValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(dbValue as Record<string, unknown>).forEach((el) => {
|
||||||
|
buildKnexQuery(
|
||||||
|
rootQuery,
|
||||||
|
dbField,
|
||||||
|
el as SearchResourceOperators,
|
||||||
|
(dbValue as Record<SearchResourceOperators, unknown>)[el as SearchResourceOperators]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orFilters.length) {
|
||||||
|
void rootQuery.andWhere((andQb) => {
|
||||||
|
return orFilters.forEach((orFilter) => {
|
||||||
|
return void andQb.orWhere((qb) => {
|
||||||
|
(Object.keys(orFilter) as K[]).forEach((key) => {
|
||||||
|
const dbField = getAttributeField(key);
|
||||||
|
if (!dbField) throw new Error(`DB field not found for ${String(key)}`);
|
||||||
|
|
||||||
|
const dbValue = orFilter[key];
|
||||||
|
if (typeof dbValue === "string" || typeof dbValue === "number") {
|
||||||
|
buildKnexQuery(qb, dbField, SearchResourceOperators.$eq, dbValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(dbValue as Record<string, unknown>).forEach((el) => {
|
||||||
|
buildKnexQuery(
|
||||||
|
qb,
|
||||||
|
dbField,
|
||||||
|
el as SearchResourceOperators,
|
||||||
|
(dbValue as Record<SearchResourceOperators, unknown>)[el as SearchResourceOperators]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
43
backend/src/lib/search-resource/search.ts
Normal file
43
backend/src/lib/search-resource/search.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export enum SearchResourceOperators {
|
||||||
|
$eq = "$eq",
|
||||||
|
$neq = "$neq",
|
||||||
|
$in = "$in",
|
||||||
|
$contains = "$contains"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchResourceOperatorSchema = z.union([
|
||||||
|
z.string(),
|
||||||
|
z.number(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[SearchResourceOperators.$eq]: z.string().optional(),
|
||||||
|
[SearchResourceOperators.$neq]: z.string().optional(),
|
||||||
|
[SearchResourceOperators.$in]: z.string().array().optional(),
|
||||||
|
[SearchResourceOperators.$contains]: z.string().array().optional()
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type TSearchResourceOperator = z.infer<typeof SearchResourceOperatorSchema>;
|
||||||
|
|
||||||
|
export type TSearchResource = {
|
||||||
|
[k: string]: z.ZodOptional<
|
||||||
|
z.ZodUnion<
|
||||||
|
[
|
||||||
|
z.ZodEffects<z.ZodString | z.ZodNumber>,
|
||||||
|
z.ZodObject<{
|
||||||
|
[SearchResourceOperators.$eq]?: z.ZodOptional<z.ZodEffects<z.ZodString | z.ZodNumber>>;
|
||||||
|
[SearchResourceOperators.$neq]?: z.ZodOptional<z.ZodEffects<z.ZodString | z.ZodNumber>>;
|
||||||
|
[SearchResourceOperators.$in]?: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString | z.ZodNumber>>>;
|
||||||
|
[SearchResourceOperators.$contains]?: z.ZodOptional<z.ZodEffects<z.ZodString>>;
|
||||||
|
}>
|
||||||
|
]
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildSearchZodSchema = <T extends TSearchResource>(schema: z.ZodObject<T>) => {
|
||||||
|
return schema.extend({ $or: schema.array().max(5).optional() }).optional();
|
||||||
|
};
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export enum CharacterType {
|
export enum CharacterType {
|
||||||
Alphabets = "alphabets",
|
Alphabets = "alphabets",
|
||||||
Numbers = "numbers",
|
Numbers = "numbers",
|
||||||
@@ -101,3 +103,10 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => {
|
|||||||
return regex.test(input);
|
return regex.test(input);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const zodValidateCharacters = (allowedCharacters: CharacterType[]) => {
|
||||||
|
const validator = characterValidator(allowedCharacters);
|
||||||
|
return (schema: z.ZodString, fieldName: string) => {
|
||||||
|
return schema.refine(validator, { message: `${fieldName} can only contain ${allowedCharacters.join(",")}` });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@@ -4,6 +4,10 @@ import PgBoss, { WorkOptions } from "pg-boss";
|
|||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import {
|
||||||
|
TSecretRotationRotateSecretsJobPayload,
|
||||||
|
TSecretRotationSendNotificationJobPayload
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
import {
|
import {
|
||||||
TScanFullRepoEventPayload,
|
TScanFullRepoEventPayload,
|
||||||
TScanPushEventPayload
|
TScanPushEventPayload
|
||||||
@@ -44,7 +48,8 @@ export enum QueueName {
|
|||||||
ProjectV3Migration = "project-v3-migration",
|
ProjectV3Migration = "project-v3-migration",
|
||||||
AccessTokenStatusUpdate = "access-token-status-update",
|
AccessTokenStatusUpdate = "access-token-status-update",
|
||||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||||
AppConnectionSecretSync = "app-connection-secret-sync"
|
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||||
|
SecretRotationV2 = "secret-rotation-v2"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@@ -73,7 +78,10 @@ export enum QueueJobs {
|
|||||||
SecretSyncSyncSecrets = "secret-sync-sync-secrets",
|
SecretSyncSyncSecrets = "secret-sync-sync-secrets",
|
||||||
SecretSyncImportSecrets = "secret-sync-import-secrets",
|
SecretSyncImportSecrets = "secret-sync-import-secrets",
|
||||||
SecretSyncRemoveSecrets = "secret-sync-remove-secrets",
|
SecretSyncRemoveSecrets = "secret-sync-remove-secrets",
|
||||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications"
|
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||||
|
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||||
|
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||||
|
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@@ -213,6 +221,19 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.SecretSyncSendActionFailedNotifications;
|
name: QueueJobs.SecretSyncSendActionFailedNotifications;
|
||||||
payload: TQueueSendSecretSyncActionFailedNotificationsDTO;
|
payload: TQueueSendSecretSyncActionFailedNotificationsDTO;
|
||||||
};
|
};
|
||||||
|
[QueueName.SecretRotationV2]:
|
||||||
|
| {
|
||||||
|
name: QueueJobs.SecretRotationV2QueueRotations;
|
||||||
|
payload: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: QueueJobs.SecretRotationV2RotateSecrets;
|
||||||
|
payload: TSecretRotationRotateSecretsJobPayload;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: QueueJobs.SecretRotationV2SendNotification;
|
||||||
|
payload: TSecretRotationSendNotificationJobPayload;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
@@ -229,6 +250,7 @@ export const queueServiceFactory = (
|
|||||||
const pgBoss = new PgBoss({
|
const pgBoss = new PgBoss({
|
||||||
connectionString: dbConnectionUrl,
|
connectionString: dbConnectionUrl,
|
||||||
archiveCompletedAfterSeconds: 60,
|
archiveCompletedAfterSeconds: 60,
|
||||||
|
cronMonitorIntervalSeconds: 5,
|
||||||
archiveFailedAfterSeconds: 1000, // we want to keep failed jobs for a longer time so that it can be retried
|
archiveFailedAfterSeconds: 1000, // we want to keep failed jobs for a longer time so that it can be retried
|
||||||
deleteAfterSeconds: 30,
|
deleteAfterSeconds: 30,
|
||||||
ssl: dbRootCert
|
ssl: dbRootCert
|
||||||
@@ -247,15 +269,12 @@ export const queueServiceFactory = (
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
const appCfg = getConfig();
|
logger.info("Initializing pg-queue...");
|
||||||
if (appCfg.SHOULD_INIT_PG_QUEUE) {
|
await pgBoss.start();
|
||||||
logger.info("Initializing pg-queue...");
|
|
||||||
await pgBoss.start();
|
|
||||||
|
|
||||||
pgBoss.on("error", (error) => {
|
pgBoss.on("error", (error) => {
|
||||||
logger.error(error, "pg-queue error");
|
logger.error(error, "pg-queue error");
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const start = <T extends QueueName>(
|
const start = <T extends QueueName>(
|
||||||
@@ -283,7 +302,7 @@ export const queueServiceFactory = (
|
|||||||
|
|
||||||
const startPg = async <T extends QueueName>(
|
const startPg = async <T extends QueueName>(
|
||||||
jobName: QueueJobs,
|
jobName: QueueJobs,
|
||||||
jobsFn: (jobs: PgBoss.Job<TQueueJobTypes[T]["payload"]>[]) => Promise<void>,
|
jobsFn: (jobs: PgBoss.JobWithMetadata<TQueueJobTypes[T]["payload"]>[]) => Promise<void>,
|
||||||
options: WorkOptions & {
|
options: WorkOptions & {
|
||||||
workerCount: number;
|
workerCount: number;
|
||||||
}
|
}
|
||||||
@@ -297,7 +316,7 @@ export const queueServiceFactory = (
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from({ length: options.workerCount }).map(() =>
|
Array.from({ length: options.workerCount }).map(() =>
|
||||||
pgBoss.work<TQueueJobTypes[T]["payload"]>(jobName, options, jobsFn)
|
pgBoss.work<TQueueJobTypes[T]["payload"]>(jobName, { ...options, includeMetadata: true }, jobsFn)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -342,6 +361,15 @@ export const queueServiceFactory = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const schedulePg = async <T extends QueueName>(
|
||||||
|
job: TQueueJobTypes[T]["name"],
|
||||||
|
cron: string,
|
||||||
|
data: TQueueJobTypes[T]["payload"],
|
||||||
|
opts?: PgBoss.ScheduleOptions & { jobId?: string }
|
||||||
|
) => {
|
||||||
|
await pgBoss.schedule(job, cron, data, opts);
|
||||||
|
};
|
||||||
|
|
||||||
const stopRepeatableJob = async <T extends QueueName>(
|
const stopRepeatableJob = async <T extends QueueName>(
|
||||||
name: T,
|
name: T,
|
||||||
job: TQueueJobTypes[T]["name"],
|
job: TQueueJobTypes[T]["name"],
|
||||||
@@ -403,6 +431,7 @@ export const queueServiceFactory = (
|
|||||||
stopJobById,
|
stopJobById,
|
||||||
getRepeatableJobs,
|
getRepeatableJobs,
|
||||||
startPg,
|
startPg,
|
||||||
queuePg
|
queuePg,
|
||||||
|
schedulePg
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -113,7 +113,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
|
|||||||
await server.register(fastifyErrHandler);
|
await server.register(fastifyErrHandler);
|
||||||
|
|
||||||
// Rate limiters and security headers
|
// Rate limiters and security headers
|
||||||
if (appCfg.isProductionMode) {
|
if (appCfg.isProductionMode && appCfg.isCloud) {
|
||||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -93,3 +93,10 @@ export const userEngagementLimit: RateLimitOptions = {
|
|||||||
max: 5,
|
max: 5,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const publicSshCaLimit: RateLimitOptions = {
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
hook: "preValidation",
|
||||||
|
max: 30, // conservative default
|
||||||
|
keyGenerator: (req) => req.realIp
|
||||||
|
};
|
||||||
|
@@ -39,3 +39,12 @@ export const GenericResourceNameSchema = z
|
|||||||
])(val),
|
])(val),
|
||||||
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
|
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const BaseSecretNameSchema = z.string().trim().min(1);
|
||||||
|
|
||||||
|
export const SecretNameSchema = BaseSecretNameSchema.refine(
|
||||||
|
(el) => !el.includes(" "),
|
||||||
|
"Secret name cannot contain spaces."
|
||||||
|
)
|
||||||
|
.refine((el) => !el.includes(":"), "Secret name cannot contain colon.")
|
||||||
|
.refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
|
||||||
|
@@ -76,6 +76,9 @@ import { secretReplicationServiceFactory } from "@app/ee/services/secret-replica
|
|||||||
import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||||
import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||||
import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
|
import { secretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal";
|
||||||
|
import { secretRotationV2QueueServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-queue";
|
||||||
|
import { secretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
|
||||||
import { gitAppDALFactory } from "@app/ee/services/secret-scanning/git-app-dal";
|
import { gitAppDALFactory } from "@app/ee/services/secret-scanning/git-app-dal";
|
||||||
import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal";
|
import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal";
|
||||||
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
|
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
|
||||||
@@ -93,6 +96,10 @@ import { sshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/s
|
|||||||
import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
|
import { sshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||||
|
import { sshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||||
|
import { sshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||||
|
import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||||
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@@ -181,6 +188,7 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co
|
|||||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||||
|
import { projectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||||
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { projectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { projectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
@@ -289,6 +297,7 @@ export const registerRoutes = async (
|
|||||||
const apiKeyDAL = apiKeyDALFactory(db);
|
const apiKeyDAL = apiKeyDALFactory(db);
|
||||||
|
|
||||||
const projectDAL = projectDALFactory(db);
|
const projectDAL = projectDALFactory(db);
|
||||||
|
const projectSshConfigDAL = projectSshConfigDALFactory(db);
|
||||||
const projectMembershipDAL = projectMembershipDALFactory(db);
|
const projectMembershipDAL = projectMembershipDALFactory(db);
|
||||||
const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db);
|
const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db);
|
||||||
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
|
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
|
||||||
@@ -382,6 +391,9 @@ export const registerRoutes = async (
|
|||||||
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
|
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
|
||||||
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
|
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
|
||||||
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
|
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
|
||||||
|
const sshHostDAL = sshHostDALFactory(db);
|
||||||
|
const sshHostLoginUserDAL = sshHostLoginUserDALFactory(db);
|
||||||
|
const sshHostLoginUserMappingDAL = sshHostLoginUserMappingDALFactory(db);
|
||||||
|
|
||||||
const kmsDAL = kmskeyDALFactory(db);
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
@@ -406,6 +418,8 @@ export const registerRoutes = async (
|
|||||||
const gatewayDAL = gatewayDALFactory(db);
|
const gatewayDAL = gatewayDALFactory(db);
|
||||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||||
|
|
||||||
|
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -791,6 +805,21 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sshHostService = sshHostServiceFactory({
|
||||||
|
userDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateBodyDAL,
|
||||||
|
sshHostDAL,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@@ -933,6 +962,7 @@ export const registerRoutes = async (
|
|||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
queueService,
|
queueService,
|
||||||
@@ -954,8 +984,10 @@ export const registerRoutes = async (
|
|||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
sshCertificateAuthorityDAL,
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
sshCertificateDAL,
|
sshCertificateDAL,
|
||||||
sshCertificateTemplateDAL,
|
sshCertificateTemplateDAL,
|
||||||
|
sshHostDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
@@ -1499,6 +1531,35 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secretRotationV2Service = secretRotationV2ServiceFactory({
|
||||||
|
secretRotationV2DAL,
|
||||||
|
permissionService,
|
||||||
|
appConnectionService,
|
||||||
|
folderDAL,
|
||||||
|
projectBotService,
|
||||||
|
licenseService,
|
||||||
|
kmsService,
|
||||||
|
auditLogService,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagV2BridgeDAL,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
keyStore,
|
||||||
|
resourceMetadataDAL,
|
||||||
|
snapshotService,
|
||||||
|
secretQueueService,
|
||||||
|
queueService
|
||||||
|
});
|
||||||
|
|
||||||
|
await secretRotationV2QueueServiceFactory({
|
||||||
|
secretRotationV2Service,
|
||||||
|
secretRotationV2DAL,
|
||||||
|
queueService,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
smtpService
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@@ -1569,6 +1630,7 @@ export const registerRoutes = async (
|
|||||||
certificate: certificateService,
|
certificate: certificateService,
|
||||||
sshCertificateAuthority: sshCertificateAuthorityService,
|
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||||
sshCertificateTemplate: sshCertificateTemplateService,
|
sshCertificateTemplate: sshCertificateTemplateService,
|
||||||
|
sshHost: sshHostService,
|
||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
@@ -1600,7 +1662,8 @@ export const registerRoutes = async (
|
|||||||
secretSync: secretSyncService,
|
secretSync: secretSyncService,
|
||||||
kmip: kmipService,
|
kmip: kmipService,
|
||||||
kmipOperation: kmipOperationService,
|
kmipOperation: kmipOperationService,
|
||||||
gateway: gatewayService
|
gateway: gatewayService,
|
||||||
|
secretRotationV2: secretRotationV2Service
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
@@ -1609,6 +1672,10 @@ export const registerRoutes = async (
|
|||||||
if (rateLimitSyncJob) {
|
if (rateLimitSyncJob) {
|
||||||
cronJobs.push(rateLimitSyncJob);
|
cronJobs.push(rateLimitSyncJob);
|
||||||
}
|
}
|
||||||
|
const licenseSyncJob = await licenseService.initializeBackgroundSync();
|
||||||
|
if (licenseSyncJob) {
|
||||||
|
cronJobs.push(licenseSyncJob);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user