mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-09 01:47:08 +00:00
Compare commits
71 Commits
feat/terra
...
main
Author | SHA1 | Date | |
---|---|---|---|
bfcfffbabf | |||
210bd220e5 | |||
7be2a10631 | |||
5753eb7d77 | |||
cb86aa40fa | |||
1131143a71 | |||
a50b8120fd | |||
f1ee53d417 | |||
229ad79f49 | |||
d7dbd01ecf | |||
026fd21fd4 | |||
9b9c1a52b3 | |||
98aa424e2e | |||
2cd5df1ab3 | |||
e0d863e06e | |||
d991af557b | |||
ae54d04357 | |||
fa590ba697 | |||
1da2896bb0 | |||
423a2f38ea | |||
db0a72f7b4 | |||
4a202d180a | |||
33103f1e95 | |||
ce8a4bc50e | |||
141a821091 | |||
b3dd5410d7 | |||
74574c6c29 | |||
4f32756951 | |||
961fe09a6e | |||
5ab853d3e6 | |||
0e073cc9fc | |||
433b1a49f0 | |||
b0b255461d | |||
c2f2dc1e72 | |||
0ee1b425df | |||
46e72e9fba | |||
06fc4e955d | |||
ece294c483 | |||
2e40ee76d0 | |||
9a712b5c85 | |||
1ec427053b | |||
6c636415bb | |||
9b083a5dfb | |||
e323cb4630 | |||
e87a1bd402 | |||
3b09173bb1 | |||
2a8e159f51 | |||
cd333a7923 | |||
e11fdf8f3a | |||
4725108319 | |||
715441908b | |||
3f190426fe | |||
954e94cd87 | |||
9dd2379fb3 | |||
6bf9ab5937 | |||
ee536717c0 | |||
a0cb4889ca | |||
271a8de4c0 | |||
b18f7b957d | |||
e6349474aa | |||
577c81be65 | |||
93baf9728b | |||
064322936b | |||
7634fc94a6 | |||
ecd39abdc1 | |||
d8313a161e | |||
d82b06c72b | |||
0088217fa9 | |||
3d072c2f48 | |||
82b828c10e | |||
766c1242fd |
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
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- "infisical-cli/v*.*.*"
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- "infisical-cli/v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
cli-integration-tests:
|
||||
name: Run tests before deployment
|
||||
uses: ./.github/workflows/run-cli-tests.yml
|
||||
secrets:
|
||||
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
||||
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
cli-integration-tests:
|
||||
name: Run tests before deployment
|
||||
uses: ./.github/workflows/run-cli-tests.yml
|
||||
secrets:
|
||||
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
||||
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
|
||||
npm-release:
|
||||
runs-on: ubuntu-latest
|
||||
npm-release:
|
||||
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:
|
||||
working-directory: ./npm
|
||||
needs:
|
||||
- cli-integration-tests
|
||||
- goreleaser
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- 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: Pack NPM
|
||||
working-directory: ${{ env.working-directory }}
|
||||
run: npm pack
|
||||
|
||||
- name: Print version
|
||||
run: echo ${{ env.CLI_VERSION }}
|
||||
- name: Publish NPM
|
||||
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
|
||||
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:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Pack NPM
|
||||
working-directory: ${{ env.working-directory }}
|
||||
run: npm pack
|
||||
|
||||
- name: Publish NPM
|
||||
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 }}
|
||||
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [cli-integration-tests]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- run: git fetch --force --tags
|
||||
- run: echo "Ref name ${{github.ref_name}}"
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ">=1.19.3"
|
||||
cache: true
|
||||
cache-dependency-path: cli/go.sum
|
||||
- name: Setup for libssl1.0-dev
|
||||
run: |
|
||||
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
|
||||
sudo apt update
|
||||
sudo apt-get install -y libssl1.0-dev
|
||||
- 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 }}
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [cli-integration-tests]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- run: git fetch --force --tags
|
||||
- run: echo "Ref name ${{github.ref_name}}"
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ">=1.19.3"
|
||||
cache: true
|
||||
cache-dependency-path: cli/go.sum
|
||||
- name: Setup for libssl1.0-dev
|
||||
run: |
|
||||
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
|
||||
sudo apt update
|
||||
sudo apt-get install -y libssl1.0-dev
|
||||
- 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
|
||||
- uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
|
||||
with:
|
||||
ruby-version: "3.3" # Not needed with a .ruby-version, .tool-versions or mise.toml
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
- name: Install deb-s3
|
||||
run: gem install deb-s3
|
||||
- name: Configure GPG Key
|
||||
run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --batch --import
|
||||
env:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||
GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
|
||||
- name: Publish to CloudSmith
|
||||
run: sh cli/upload_to_cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }}
|
||||
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
|
||||
|
85
backend/Dockerfile.dev.fips
Normal file
85
backend/Dockerfile.dev.fips
Normal file
@ -0,0 +1,85 @@
|
||||
FROM node:20-slim
|
||||
|
||||
# ? Setup a test SoftHSM module. In production a real HSM is used.
|
||||
|
||||
ARG SOFTHSM2_VERSION=2.5.0
|
||||
|
||||
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
||||
SOFTHSM2_SOURCES=/tmp/softhsm2
|
||||
|
||||
# Install build dependencies including python3 (required for pkcs11js and partially TDS driver)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
autoconf \
|
||||
automake \
|
||||
git \
|
||||
libtool \
|
||||
libssl-dev \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
openssh-client \
|
||||
curl \
|
||||
pkg-config \
|
||||
perl \
|
||||
wget
|
||||
|
||||
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apt-get install -y \
|
||||
unixodbc \
|
||||
unixodbc-dev \
|
||||
freetds-dev \
|
||||
freetds-bin \
|
||||
tdsodbc
|
||||
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
# Build and install SoftHSM2
|
||||
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
||||
WORKDIR ${SOFTHSM2_SOURCES}
|
||||
|
||||
RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \
|
||||
&& sh autogen.sh \
|
||||
&& ./configure --prefix=/usr/local --disable-gost \
|
||||
&& make \
|
||||
&& make install
|
||||
|
||||
WORKDIR /root
|
||||
RUN rm -fr ${SOFTHSM2_SOURCES}
|
||||
|
||||
# Install pkcs11-tool
|
||||
RUN apt-get install -y opensc
|
||||
|
||||
RUN mkdir -p /etc/softhsm2/tokens && \
|
||||
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
||||
|
||||
WORKDIR /openssl-build
|
||||
RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
||||
&& tar -xf openssl-3.1.2.tar.gz \
|
||||
&& cd openssl-3.1.2 \
|
||||
&& ./Configure enable-fips \
|
||||
&& make \
|
||||
&& make install_fips
|
||||
|
||||
# ? App setup
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||
apt-get update && \
|
||||
apt-get install -y infisical=0.8.1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package.json
|
||||
COPY package-lock.json package-lock.json
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV OPENSSL_CONF=/app/nodejs.cnf
|
||||
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
|
||||
ENV NODE_OPTIONS=--force-fips
|
||||
|
||||
CMD ["npm", "run", "dev:docker"]
|
@ -11,6 +11,7 @@ export const mockQueue = (): TQueueServiceFactory => {
|
||||
job[name] = jobData;
|
||||
},
|
||||
queuePg: async () => {},
|
||||
schedulePg: async () => {},
|
||||
initialize: async () => {},
|
||||
shutdown: async () => undefined,
|
||||
stopRepeatableJob: async () => true,
|
||||
|
16
backend/nodejs.cnf
Normal file
16
backend/nodejs.cnf
Normal file
@ -0,0 +1,16 @@
|
||||
nodejs_conf = nodejs_init
|
||||
|
||||
.include /usr/local/ssl/fipsmodule.cnf
|
||||
|
||||
[nodejs_init]
|
||||
providers = provider_sect
|
||||
|
||||
[provider_sect]
|
||||
default = default_sect
|
||||
fips = fips_sect
|
||||
|
||||
[default_sect]
|
||||
activate = 1
|
||||
|
||||
[algorithm_sect]
|
||||
default_properties = fips=yes
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -33,6 +33,7 @@ import { TScimServiceFactory } from "@app/ee/services/scim/scim-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 { 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 { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||
@ -237,6 +238,7 @@ declare module "fastify" {
|
||||
kmip: TKmipServiceFactory;
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
52
backend/src/@types/knex.d.ts
vendored
52
backend/src/@types/knex.d.ts
vendored
@ -17,6 +17,9 @@ import {
|
||||
TApiKeys,
|
||||
TApiKeysInsert,
|
||||
TApiKeysUpdate,
|
||||
TAppConnections,
|
||||
TAppConnectionsInsert,
|
||||
TAppConnectionsUpdate,
|
||||
TAuditLogs,
|
||||
TAuditLogsInsert,
|
||||
TAuditLogStreams,
|
||||
@ -65,6 +68,9 @@ import {
|
||||
TDynamicSecrets,
|
||||
TDynamicSecretsInsert,
|
||||
TDynamicSecretsUpdate,
|
||||
TExternalGroupOrgRoleMappings,
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate,
|
||||
TExternalKms,
|
||||
TExternalKmsInsert,
|
||||
TExternalKmsUpdate,
|
||||
@ -299,6 +305,12 @@ import {
|
||||
TSecretRotations,
|
||||
TSecretRotationsInsert,
|
||||
TSecretRotationsUpdate,
|
||||
TSecretRotationsV2,
|
||||
TSecretRotationsV2Insert,
|
||||
TSecretRotationsV2Update,
|
||||
TSecretRotationV2SecretMappings,
|
||||
TSecretRotationV2SecretMappingsInsert,
|
||||
TSecretRotationV2SecretMappingsUpdate,
|
||||
TSecrets,
|
||||
TSecretScanningGitRisks,
|
||||
TSecretScanningGitRisksInsert,
|
||||
@ -320,15 +332,27 @@ import {
|
||||
TSecretSnapshotsInsert,
|
||||
TSecretSnapshotsUpdate,
|
||||
TSecretsUpdate,
|
||||
TSecretsV2,
|
||||
TSecretsV2Insert,
|
||||
TSecretsV2Update,
|
||||
TSecretSyncs,
|
||||
TSecretSyncsInsert,
|
||||
TSecretSyncsUpdate,
|
||||
TSecretTagJunction,
|
||||
TSecretTagJunctionInsert,
|
||||
TSecretTagJunctionUpdate,
|
||||
TSecretTags,
|
||||
TSecretTagsInsert,
|
||||
TSecretTagsUpdate,
|
||||
TSecretV2TagJunction,
|
||||
TSecretV2TagJunctionInsert,
|
||||
TSecretV2TagJunctionUpdate,
|
||||
TSecretVersions,
|
||||
TSecretVersionsInsert,
|
||||
TSecretVersionsUpdate,
|
||||
TSecretVersionsV2,
|
||||
TSecretVersionsV2Insert,
|
||||
TSecretVersionsV2Update,
|
||||
TSecretVersionTagJunction,
|
||||
TSecretVersionTagJunctionInsert,
|
||||
TSecretVersionTagJunctionUpdate,
|
||||
@ -387,24 +411,6 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} 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" {
|
||||
namespace Knex {
|
||||
@ -950,5 +956,15 @@ declare module "knex/types/tables" {
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate
|
||||
>;
|
||||
[TableName.SecretRotationV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretRotationsV2,
|
||||
TSecretRotationsV2Insert,
|
||||
TSecretRotationsV2Update
|
||||
>;
|
||||
[TableName.SecretRotationV2SecretMapping]: KnexOriginal.CompositeTableType<
|
||||
TSecretRotationV2SecretMappings,
|
||||
TSecretRotationV2SecretMappingsInsert,
|
||||
TSecretRotationV2SecretMappingsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
|
@ -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);
|
||||
}
|
@ -19,7 +19,8 @@ export const AppConnectionsSchema = z.object({
|
||||
version: z.number().default(1),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
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-reviewers";
|
||||
export * from "./api-keys";
|
||||
export * from "./app-connections";
|
||||
export * from "./audit-log-streams";
|
||||
export * from "./audit-logs";
|
||||
export * from "./auth-token-sessions";
|
||||
@ -19,6 +20,7 @@ export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-group-org-role-mappings";
|
||||
export * from "./external-kms";
|
||||
export * from "./gateways";
|
||||
export * from "./git-app-install-sessions";
|
||||
@ -97,13 +99,16 @@ export * from "./secret-references";
|
||||
export * from "./secret-references-v2";
|
||||
export * from "./secret-rotation-output-v2";
|
||||
export * from "./secret-rotation-outputs";
|
||||
export * from "./secret-rotation-v2-secret-mappings";
|
||||
export * from "./secret-rotations";
|
||||
export * from "./secret-rotations-v2";
|
||||
export * from "./secret-scanning-git-risks";
|
||||
export * from "./secret-sharing";
|
||||
export * from "./secret-snapshot-folders";
|
||||
export * from "./secret-snapshot-secrets";
|
||||
export * from "./secret-snapshot-secrets-v2";
|
||||
export * from "./secret-snapshots";
|
||||
export * from "./secret-syncs";
|
||||
export * from "./secret-tag-junction";
|
||||
export * from "./secret-tags";
|
||||
export * from "./secret-v2-tag-junction";
|
||||
|
@ -140,7 +140,9 @@ export enum TableName {
|
||||
KmipClient = "kmip_clients",
|
||||
KmipOrgConfig = "kmip_org_configs",
|
||||
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";
|
||||
@ -233,3 +235,8 @@ export enum ActionProjectType {
|
||||
// project operations that happen on all types
|
||||
Any = "any"
|
||||
}
|
||||
|
||||
export enum SortDirection {
|
||||
ASC = "asc",
|
||||
DESC = "desc"
|
||||
}
|
||||
|
@ -23,10 +23,10 @@ export const OrganizationsSchema = z.object({
|
||||
defaultMembershipRole: z.string().default("member"),
|
||||
enforceMfa: z.boolean().default(false),
|
||||
selectedMfaMethod: z.string().nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
|
||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
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>>;
|
@ -277,8 +277,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
||||
secretPath: z.string(),
|
||||
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({
|
||||
secretValue: z.string().optional(),
|
||||
isRotatedSecret: z.boolean().optional(),
|
||||
op: z.string(),
|
||||
tags: SanitizedTagSchema.array().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.nullish(),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -40,16 +41,10 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretRotation = await server.services.secretRotation.createRotation({
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
projectId: req.body.workspaceId
|
||||
handler: async () => {
|
||||
throw new BadRequestError({
|
||||
message: `This version of Secret Rotations has been deprecated. Please see docs for new version.`
|
||||
});
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -33,7 +33,8 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretId: z.string(),
|
||||
tags: SanitizedTagSchema.array()
|
||||
tags: SanitizedTagSchema.array(),
|
||||
isRotatedSecret: z.boolean().optional()
|
||||
})
|
||||
.array(),
|
||||
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
||||
|
@ -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 { registerProjectRoleRouter } from "./project-role-router";
|
||||
|
||||
@ -13,4 +18,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerIdentityProjectAdditionalPrivilegeRouter, {
|
||||
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 };
|
||||
}
|
||||
});
|
||||
};
|
@ -2,6 +2,13 @@ import {
|
||||
TCreateProjectTemplateDTO,
|
||||
TUpdateProjectTemplateDTO
|
||||
} 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 { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
@ -56,6 +63,8 @@ export type TCreateAuditLogDTO = {
|
||||
projectId?: string;
|
||||
} & BaseAuthData;
|
||||
|
||||
export type AuditLogInfo = Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
||||
|
||||
interface BaseAuthData {
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
@ -285,7 +294,17 @@ export enum EventType {
|
||||
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
||||
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||
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"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@ -2277,6 +2296,15 @@ interface KmipOperationRegisterEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectAccessRequestEvent {
|
||||
type: EventType.PROJECT_ACCESS_REQUEST;
|
||||
metadata: {
|
||||
projectId: string;
|
||||
requesterId: string;
|
||||
requesterEmail: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SetupKmipEvent {
|
||||
type: EventType.SETUP_KMIP;
|
||||
metadata: {
|
||||
@ -2302,6 +2330,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 =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -2511,5 +2596,13 @@ export type Event =
|
||||
| KmipOperationRevokeEvent
|
||||
| KmipOperationLocateEvent
|
||||
| KmipOperationRegisterEvent
|
||||
| ProjectAccessRequestEvent
|
||||
| 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) => {
|
||||
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(
|
||||
(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
|
||||
@ -40,7 +42,7 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
||||
inputHostIps.push(...resolvedIps);
|
||||
}
|
||||
|
||||
if (!isGateway) {
|
||||
if (!isGateway && !appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP) {
|
||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
secretApproval: false,
|
||||
secretRotation: true,
|
||||
secretRotation: false,
|
||||
caCrl: false,
|
||||
instanceUserManagement: false,
|
||||
externalKms: false,
|
||||
|
@ -5,6 +5,7 @@
|
||||
// TODO(akhilmhdh): With tony find out the api structure and fill it here
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { CronJob } from "cron";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
@ -85,6 +86,20 @@ export const licenseServiceFactory = ({
|
||||
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 () => {
|
||||
try {
|
||||
if (appCfg.LICENSE_SERVER_KEY) {
|
||||
@ -98,10 +113,7 @@ export const licenseServiceFactory = ({
|
||||
if (appCfg.LICENSE_KEY) {
|
||||
const token = await licenseServerOnPremApi.refreshLicense();
|
||||
if (token) {
|
||||
const {
|
||||
data: { currentPlan }
|
||||
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan");
|
||||
onPremFeatures = currentPlan;
|
||||
await syncLicenseKeyOnPremFeatures(true);
|
||||
instanceType = InstanceType.EnterpriseOnPrem;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
||||
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) => {
|
||||
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
|
||||
try {
|
||||
@ -662,6 +683,7 @@ export const licenseServiceFactory = ({
|
||||
getOrgTaxInvoices,
|
||||
getOrgTaxIds,
|
||||
addOrgTaxId,
|
||||
delOrgTaxId
|
||||
delOrgTaxId,
|
||||
initializeBackgroundSync
|
||||
};
|
||||
};
|
||||
|
@ -56,7 +56,7 @@ export type TFeatureSet = {
|
||||
trial_end: null;
|
||||
has_used_trial: true;
|
||||
secretApproval: false;
|
||||
secretRotation: true;
|
||||
secretRotation: false;
|
||||
caCrl: false;
|
||||
instanceUserManagement: false;
|
||||
externalKms: false;
|
||||
|
@ -77,6 +77,15 @@ export enum ProjectPermissionSecretSyncActions {
|
||||
RemoveSecrets = "remove-secrets"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretRotationActions {
|
||||
Read = "read",
|
||||
ReadGeneratedCredentials = "read-generated-credentials",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
RotateSecrets = "rotate-secrets"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionKmipActions {
|
||||
CreateClients = "create-clients",
|
||||
UpdateClients = "update-clients",
|
||||
@ -142,6 +151,11 @@ export type SecretImportSubjectFields = {
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type SecretRotationsSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type IdentityManagementSubjectFields = {
|
||||
identityId: string;
|
||||
};
|
||||
@ -184,7 +198,13 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [
|
||||
ProjectPermissionSecretRotationActions,
|
||||
(
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretRotation> & SecretRotationsSubjectFields)
|
||||
)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||
@ -300,12 +320,6 @@ const GeneralPermissionSchema = [
|
||||
"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({
|
||||
subject: z.literal(ProjectPermissionSub.SecretRollback).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
|
||||
@ -487,6 +501,12 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
||||
"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
|
||||
]);
|
||||
|
||||
@ -541,6 +561,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
"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
|
||||
]);
|
||||
|
||||
@ -554,7 +584,6 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretFolders,
|
||||
ProjectPermissionSub.SecretImports,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.SecretRotation,
|
||||
ProjectPermissionSub.Role,
|
||||
ProjectPermissionSub.Integrations,
|
||||
ProjectPermissionSub.Webhooks,
|
||||
@ -678,6 +707,18 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
ProjectPermissionSecretRotationActions.Edit,
|
||||
ProjectPermissionSecretRotationActions.Delete,
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
|
||||
ProjectPermissionSecretRotationActions.RotateSecrets
|
||||
],
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@ -727,7 +768,7 @@ const buildMemberPermissionRules = () => {
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
|
||||
@ -873,7 +914,7 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
|
@ -257,6 +257,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema("secVerTag")
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
@ -285,7 +290,8 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
);
|
||||
)
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: doc,
|
||||
key: "id",
|
||||
@ -304,14 +310,16 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secretId",
|
||||
label: "secret" as const,
|
||||
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) =>
|
||||
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId, rotationId }) =>
|
||||
secretId
|
||||
? {
|
||||
id: secretId,
|
||||
version: orgSecVersion,
|
||||
key: orgSecKey,
|
||||
encryptedValue: orgSecValue,
|
||||
encryptedComment: orgSecComment
|
||||
encryptedComment: orgSecComment,
|
||||
isRotatedSecret: Boolean(rotationId),
|
||||
rotationId
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
|
@ -262,7 +262,13 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
id: el.id,
|
||||
version: el.version,
|
||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
isRotatedSecret: el.secret.isRotatedSecret,
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
secretValue: el.secret.isRotatedSecret
|
||||
? undefined
|
||||
: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: "",
|
||||
@ -609,7 +615,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => {
|
||||
const encryptedValue =
|
||||
typeof el.encryptedValue !== "undefined"
|
||||
!el.secret.isRotatedSecret && typeof el.encryptedValue !== "undefined"
|
||||
? {
|
||||
encryptedValue: el.encryptedValue as Buffer,
|
||||
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 { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSub
|
||||
} from "../permission/project-permission";
|
||||
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
||||
@ -69,7 +69,10 @@ export const secretRotationServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
return {
|
||||
custom: [],
|
||||
@ -99,7 +102,7 @@ export const secretRotationServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
@ -208,7 +211,10 @@ export const secretRotationServiceFactory = ({
|
||||
actorOrgId,
|
||||
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);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const docs = await secretRotationDAL.findSecretV2({ projectId });
|
||||
@ -254,7 +260,10 @@ export const secretRotationServiceFactory = ({
|
||||
actorOrgId,
|
||||
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.addToQueue(doc.id, doc.interval);
|
||||
return doc;
|
||||
@ -273,7 +282,7 @@ export const secretRotationServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSecretRotationActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
const deletedDoc = await secretRotationDAL.transaction(async (tx) => {
|
||||
|
@ -398,8 +398,32 @@ export const secretSnapshotServiceFactory = ({
|
||||
if (shouldUseBridge) {
|
||||
const rollback = await snapshotDAL.transaction(async (tx) => {
|
||||
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx);
|
||||
// this will remove all secrets in current folder
|
||||
const deletedTopLevelSecs = await secretV2BridgeDAL.delete({ folderId: snapshot.folderId }, tx);
|
||||
const secretRotationIds = rollbackSnaps
|
||||
.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);
|
||||
// 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
|
||||
@ -424,28 +448,31 @@ export const secretSnapshotServiceFactory = ({
|
||||
);
|
||||
const secrets = await secretV2BridgeDAL.insertMany(
|
||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||
secretVersions.map(
|
||||
({
|
||||
latestSecretVersion,
|
||||
version,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
secretId,
|
||||
envId,
|
||||
id,
|
||||
tags,
|
||||
// exclude the bottom fields from the secret - they are for versioning only.
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType,
|
||||
...el
|
||||
}) => ({
|
||||
...el,
|
||||
id: secretId,
|
||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||
folderId
|
||||
})
|
||||
)
|
||||
secretVersions
|
||||
.filter((v) => !v.isRotatedSecret)
|
||||
.map(
|
||||
({
|
||||
latestSecretVersion,
|
||||
version,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
secretId,
|
||||
envId,
|
||||
id,
|
||||
tags,
|
||||
// exclude the bottom fields from the secret - they are for versioning only.
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType,
|
||||
isRotatedSecret,
|
||||
...el
|
||||
}) => ({
|
||||
...el,
|
||||
id: secretId,
|
||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||
folderId
|
||||
})
|
||||
)
|
||||
),
|
||||
tx
|
||||
);
|
||||
|
@ -181,6 +181,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SnapshotFolder}.folderVersionId`,
|
||||
`${TableName.SecretFolderVersion}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`,
|
||||
`${TableName.SecretVersionV2}.secretId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretVersionV2))
|
||||
.select(
|
||||
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.SecretVersionV2Tag).as("tagVersionId"),
|
||||
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({
|
||||
data,
|
||||
@ -221,7 +227,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "id",
|
||||
label: "secretVersions" as const,
|
||||
mapper: (el) => SecretVersionsV2Schema.parse(el),
|
||||
mapper: (el) => ({
|
||||
...SecretVersionsV2Schema.parse(el),
|
||||
isRotatedSecret: Boolean(el.rotationId),
|
||||
rotationId: el.rotationId
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagVersionId",
|
||||
@ -476,6 +486,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretVersionV2}.secretId`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.leftJoin<{ latestSecretVersion: number }>(
|
||||
(tx || db)(TableName.SecretVersionV2)
|
||||
.groupBy("secretId")
|
||||
@ -506,7 +521,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||
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({
|
||||
@ -523,7 +539,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
label: "secretVersions" as const,
|
||||
mapper: (el) => ({
|
||||
...SecretVersionsV2Schema.parse(el),
|
||||
latestSecretVersion: el.latestSecretVersion as number
|
||||
latestSecretVersion: el.latestSecretVersion as number,
|
||||
isRotatedSecret: Boolean(el.rotationId)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
|
@ -8,7 +8,8 @@ export const PgSqlLock = {
|
||||
SuperAdminInit: 2024,
|
||||
KmsRootKeyInit: 2025,
|
||||
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;
|
||||
|
||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||
@ -33,6 +34,7 @@ export const KeyStorePrefixes = {
|
||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` 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,
|
||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||
`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 { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@ -819,7 +824,8 @@ export const DASHBOARD = {
|
||||
includeSecrets: "Whether to include project secrets in the response.",
|
||||
includeFolders: "Whether to include project folders 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: {
|
||||
projectId: "The ID of the project to list secrets/folders from.",
|
||||
@ -834,7 +840,8 @@ export const DASHBOARD = {
|
||||
includeSecrets: "Whether to include project secrets in the response.",
|
||||
includeFolders: "Whether to include project folders 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;
|
||||
|
||||
@ -1659,7 +1666,8 @@ export const AppConnections = {
|
||||
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||
description: `An optional description for the ${appName} Connection.`,
|
||||
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) => {
|
||||
@ -1669,12 +1677,25 @@ export const AppConnections = {
|
||||
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||
description: `The updated description of the ${appName} Connection.`,
|
||||
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) => ({
|
||||
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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const SecretSyncs = {
|
||||
@ -1788,13 +1809,73 @@ export const SecretSyncs = {
|
||||
org: "The ID of the Humanitec org 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."
|
||||
},
|
||||
TERRAFORM_CLOUD: {
|
||||
org: "The ID of the Terraform Cloud org to sync secrets to.",
|
||||
destinationName: "The name of the Terraform Cloud variable set / workspace to sync secrets to.",
|
||||
destinationId: "The ID of the Terraform Cloud variable set / 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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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()),
|
||||
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
|
||||
HTTPS_ENABLED: zodStrBool,
|
||||
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||
@ -192,6 +193,7 @@ const envSchema = z
|
||||
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"),
|
||||
/* ----------------------------------------------------------------------------- */
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
@ -262,6 +264,7 @@ const envSchema = z
|
||||
isSmtpConfigured: Boolean(data.SMTP_HOST),
|
||||
isRedisConfigured: Boolean(data.REDIS_URL),
|
||||
isDevelopmentMode: data.NODE_ENV === "development",
|
||||
isRotationDevelopmentMode: data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE,
|
||||
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
|
||||
|
||||
isSecretScanningConfigured:
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { URL } from "url"; // Import the URL class
|
||||
|
||||
export const getDbConnectionHost = (urlString: string) => {
|
||||
export const getDbConnectionHost = (urlString?: string) => {
|
||||
if (!urlString) return null;
|
||||
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
// Split hostname and port (if provided)
|
||||
|
@ -2,11 +2,17 @@ import { Knex } from "knex";
|
||||
|
||||
import { UnauthorizedError } from "../errors";
|
||||
|
||||
type TKnexDynamicPrimitiveOperator<T extends object> = {
|
||||
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
||||
value: string;
|
||||
field: Extract<keyof T, string>;
|
||||
};
|
||||
type TKnexDynamicPrimitiveOperator<T extends object> =
|
||||
| {
|
||||
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
||||
value: string;
|
||||
field: Extract<keyof T, string>;
|
||||
}
|
||||
| {
|
||||
operator: "notIn";
|
||||
value: string[];
|
||||
field: Extract<keyof T, string>;
|
||||
};
|
||||
|
||||
type TKnexDynamicInOperator<T extends object> = {
|
||||
operator: "in";
|
||||
@ -48,6 +54,10 @@ export const buildDynamicKnexQuery = <T extends object>(
|
||||
void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`);
|
||||
break;
|
||||
}
|
||||
case "notIn": {
|
||||
void queryBuilder.whereNotIn(filterAst.field, filterAst.value);
|
||||
break;
|
||||
}
|
||||
case "and": {
|
||||
filterAst.value.forEach((el) => {
|
||||
void queryBuilder.andWhere((subQueryBuilder) => {
|
||||
|
@ -7,7 +7,7 @@ export const prependTableNameToFindFilter = (tableName: TableName, filterObj: ob
|
||||
Object.fromEntries(
|
||||
Object.entries(filterObj).map(([key, value]) =>
|
||||
key.startsWith("$")
|
||||
? [key, prependTableNameToFindFilter(tableName, value as object)]
|
||||
? [key, value ? prependTableNameToFindFilter(tableName, value as object) : value]
|
||||
: [`${tableName}.${key}`, value]
|
||||
)
|
||||
);
|
||||
|
@ -36,7 +36,8 @@ export enum CharacterType {
|
||||
DoubleQuote = "doubleQuote", // "
|
||||
Comma = "comma", // ,
|
||||
Semicolon = "semicolon", // ;
|
||||
Exclamation = "exclamation" // !
|
||||
Exclamation = "exclamation", // !
|
||||
Fullstop = "fullStop" // .
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,7 +82,8 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => {
|
||||
[CharacterType.DoubleQuote]: '\\"',
|
||||
[CharacterType.Comma]: ",",
|
||||
[CharacterType.Semicolon]: ";",
|
||||
[CharacterType.Exclamation]: "!"
|
||||
[CharacterType.Exclamation]: "!",
|
||||
[CharacterType.Fullstop]: "."
|
||||
};
|
||||
|
||||
// Combine patterns from allowed characters
|
||||
|
@ -4,6 +4,10 @@ import PgBoss, { WorkOptions } from "pg-boss";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||
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 {
|
||||
TScanFullRepoEventPayload,
|
||||
TScanPushEventPayload
|
||||
@ -44,7 +48,8 @@ export enum QueueName {
|
||||
ProjectV3Migration = "project-v3-migration",
|
||||
AccessTokenStatusUpdate = "access-token-status-update",
|
||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||
AppConnectionSecretSync = "app-connection-secret-sync"
|
||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||
SecretRotationV2 = "secret-rotation-v2"
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@ -73,7 +78,10 @@ export enum QueueJobs {
|
||||
SecretSyncSyncSecrets = "secret-sync-sync-secrets",
|
||||
SecretSyncImportSecrets = "secret-sync-import-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 = {
|
||||
@ -213,6 +221,19 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.SecretSyncSendActionFailedNotifications;
|
||||
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>;
|
||||
@ -229,6 +250,7 @@ export const queueServiceFactory = (
|
||||
const pgBoss = new PgBoss({
|
||||
connectionString: dbConnectionUrl,
|
||||
archiveCompletedAfterSeconds: 60,
|
||||
cronMonitorIntervalSeconds: 5,
|
||||
archiveFailedAfterSeconds: 1000, // we want to keep failed jobs for a longer time so that it can be retried
|
||||
deleteAfterSeconds: 30,
|
||||
ssl: dbRootCert
|
||||
@ -247,15 +269,12 @@ export const queueServiceFactory = (
|
||||
>;
|
||||
|
||||
const initialize = async () => {
|
||||
const appCfg = getConfig();
|
||||
if (appCfg.SHOULD_INIT_PG_QUEUE) {
|
||||
logger.info("Initializing pg-queue...");
|
||||
await pgBoss.start();
|
||||
logger.info("Initializing pg-queue...");
|
||||
await pgBoss.start();
|
||||
|
||||
pgBoss.on("error", (error) => {
|
||||
logger.error(error, "pg-queue error");
|
||||
});
|
||||
}
|
||||
pgBoss.on("error", (error) => {
|
||||
logger.error(error, "pg-queue error");
|
||||
});
|
||||
};
|
||||
|
||||
const start = <T extends QueueName>(
|
||||
@ -283,7 +302,7 @@ export const queueServiceFactory = (
|
||||
|
||||
const startPg = async <T extends QueueName>(
|
||||
jobName: QueueJobs,
|
||||
jobsFn: (jobs: PgBoss.Job<TQueueJobTypes[T]["payload"]>[]) => Promise<void>,
|
||||
jobsFn: (jobs: PgBoss.JobWithMetadata<TQueueJobTypes[T]["payload"]>[]) => Promise<void>,
|
||||
options: WorkOptions & {
|
||||
workerCount: number;
|
||||
}
|
||||
@ -297,7 +316,7 @@ export const queueServiceFactory = (
|
||||
|
||||
await Promise.all(
|
||||
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>(
|
||||
name: T,
|
||||
job: TQueueJobTypes[T]["name"],
|
||||
@ -403,6 +431,7 @@ export const queueServiceFactory = (
|
||||
stopJobById,
|
||||
getRepeatableJobs,
|
||||
startPg,
|
||||
queuePg
|
||||
queuePg,
|
||||
schedulePg
|
||||
};
|
||||
};
|
||||
|
@ -39,3 +39,10 @@ export const GenericResourceNameSchema = z
|
||||
])(val),
|
||||
"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.");
|
||||
|
@ -76,6 +76,9 @@ import { secretReplicationServiceFactory } from "@app/ee/services/secret-replica
|
||||
import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||
import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||
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 { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal";
|
||||
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
|
||||
@ -406,6 +409,8 @@ export const registerRoutes = async (
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
|
||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@ -662,6 +667,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
smtpService,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
projectUserMembershipRoleDAL,
|
||||
@ -964,7 +970,8 @@ export const registerRoutes = async (
|
||||
projectSlackConfigDAL,
|
||||
slackIntegrationDAL,
|
||||
projectTemplateService,
|
||||
groupProjectDAL
|
||||
groupProjectDAL,
|
||||
smtpService
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
@ -1497,6 +1504,35 @@ export const registerRoutes = async (
|
||||
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();
|
||||
|
||||
// setup the communication with license key server
|
||||
@ -1598,7 +1634,8 @@ export const registerRoutes = async (
|
||||
secretSync: secretSyncService,
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService
|
||||
gateway: gatewayService,
|
||||
secretRotationV2: secretRotationV2Service
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
@ -1607,6 +1644,10 @@ export const registerRoutes = async (
|
||||
if (rateLimitSyncJob) {
|
||||
cronJobs.push(rateLimitSyncJob);
|
||||
}
|
||||
const licenseSyncJob = await licenseService.initializeBackgroundSync();
|
||||
if (licenseSyncJob) {
|
||||
cronJobs.push(licenseSyncJob);
|
||||
}
|
||||
}
|
||||
|
||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||
|
@ -134,7 +134,9 @@ export const secretRawSchema = z.object({
|
||||
membershipId: z.string().nullable().optional()
|
||||
})
|
||||
.optional()
|
||||
.nullable()
|
||||
.nullable(),
|
||||
isRotatedSecret: z.boolean().optional(),
|
||||
rotationId: z.string().uuid().nullish()
|
||||
});
|
||||
|
||||
export const ProjectPermissionSchema = z.object({
|
||||
|
@ -24,8 +24,14 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
method: I["method"];
|
||||
credentials: I["credentials"];
|
||||
description?: string | null;
|
||||
isPlatformManagedCredentials?: boolean;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
name?: string;
|
||||
credentials?: I["credentials"];
|
||||
description?: string | null;
|
||||
isPlatformManagedCredentials?: boolean;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
||||
sanitizedResponseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||
@ -208,10 +214,10 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { name, method, credentials, description } = req.body;
|
||||
const { name, method, credentials, description, isPlatformManagedCredentials } = req.body;
|
||||
|
||||
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||
{ name, method, app, credentials, description },
|
||||
{ name, method, app, credentials, description, isPlatformManagedCredentials },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
@ -224,7 +230,8 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
name,
|
||||
method,
|
||||
app,
|
||||
connectionId: appConnection.id
|
||||
connectionId: appConnection.id,
|
||||
isPlatformManagedCredentials
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -251,11 +258,11 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { name, credentials, description } = req.body;
|
||||
const { name, credentials, description, isPlatformManagedCredentials } = req.body;
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const appConnection = (await server.services.appConnection.updateAppConnection(
|
||||
{ name, credentials, connectionId, description },
|
||||
{ name, credentials, connectionId, description, isPlatformManagedCredentials },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
@ -268,7 +275,8 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
name,
|
||||
description,
|
||||
credentialsUpdated: Boolean(credentials),
|
||||
connectionId
|
||||
connectionId,
|
||||
isPlatformManagedCredentials
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -22,10 +22,11 @@ import {
|
||||
HumanitecConnectionListItemSchema,
|
||||
SanitizedHumanitecConnectionSchema
|
||||
} from "@app/services/app-connection/humanitec";
|
||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||
import {
|
||||
SanitizedTerraformCloudConnectionSchema,
|
||||
TerraformCloudConnectionListItemSchema
|
||||
} from "@app/services/app-connection/terraform-cloud";
|
||||
PostgresConnectionListItemSchema,
|
||||
SanitizedPostgresConnectionSchema
|
||||
} from "@app/services/app-connection/postgres";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// can't use discriminated due to multiple schemas for certain apps
|
||||
@ -37,7 +38,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||
...SanitizedDatabricksConnectionSchema.options,
|
||||
...SanitizedHumanitecConnectionSchema.options,
|
||||
...SanitizedTerraformCloudConnectionSchema.options
|
||||
...SanitizedPostgresConnectionSchema.options,
|
||||
...SanitizedMsSqlConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@ -48,7 +50,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
DatabricksConnectionListItemSchema,
|
||||
HumanitecConnectionListItemSchema,
|
||||
TerraformCloudConnectionListItemSchema
|
||||
PostgresConnectionListItemSchema,
|
||||
MsSqlConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
@ -7,7 +7,8 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
|
||||
export * from "./app-connection-router";
|
||||
|
||||
@ -20,5 +21,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter
|
||||
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter
|
||||
};
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateMsSqlConnectionSchema,
|
||||
SanitizedMsSqlConnectionSchema,
|
||||
UpdateMsSqlConnectionSchema
|
||||
} from "@app/services/app-connection/mssql";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerMsSqlConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.MsSql,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedMsSqlConnectionSchema,
|
||||
createSchema: CreateMsSqlConnectionSchema,
|
||||
updateSchema: UpdateMsSqlConnectionSchema
|
||||
});
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreatePostgresConnectionSchema,
|
||||
SanitizedPostgresConnectionSchema,
|
||||
UpdatePostgresConnectionSchema
|
||||
} from "@app/services/app-connection/postgres";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerPostgresConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Postgres,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedPostgresConnectionSchema,
|
||||
createSchema: CreatePostgresConnectionSchema,
|
||||
updateSchema: UpdatePostgresConnectionSchema
|
||||
});
|
||||
};
|
@ -1,69 +0,0 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateTerraformCloudConnectionSchema,
|
||||
SanitizedTerraformCloudConnectionSchema,
|
||||
TTerraformCloudOrganization,
|
||||
UpdateTerraformCloudConnectionSchema
|
||||
} from "@app/services/app-connection/terraform-cloud";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerTerraformCloudConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.TerraformCloud,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedTerraformCloudConnectionSchema,
|
||||
createSchema: CreateTerraformCloudConnectionSchema,
|
||||
updateSchema: UpdateTerraformCloudConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/organizations`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
variableSets: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
global: z.boolean().optional()
|
||||
})
|
||||
.array(),
|
||||
workspaces: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const organizations: TTerraformCloudOrganization[] =
|
||||
await server.services.appConnection.terraformCloud.listOrganizations(connectionId, req.permission);
|
||||
|
||||
return organizations;
|
||||
}
|
||||
});
|
||||
};
|
@ -8,6 +8,7 @@ import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
import { DASHBOARD } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
@ -101,12 +102,30 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
||||
includeImports: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeImports),
|
||||
includeSecretRotations: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecretRotations),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
folders: SecretFoldersSchema.extend({ environment: z.string() }).array().optional(),
|
||||
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
|
||||
secretRotations: z
|
||||
.intersection(
|
||||
SecretRotationV2Schema,
|
||||
z.object({
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
tags: SanitizedTagSchema.array().optional()
|
||||
})
|
||||
.nullable()
|
||||
.array()
|
||||
})
|
||||
)
|
||||
.array()
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
@ -127,6 +146,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
totalImportCount: z.number().optional(),
|
||||
totalSecretRotationCount: z.number().optional(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
@ -144,7 +164,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
includeFolders,
|
||||
includeSecrets,
|
||||
includeImports,
|
||||
includeDynamicSecrets
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations
|
||||
} = req.query;
|
||||
|
||||
const environments = req.query.environments.split(",");
|
||||
@ -166,11 +187,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
let dynamicSecrets:
|
||||
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
|
||||
| undefined;
|
||||
let secretRotations:
|
||||
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
|
||||
| undefined;
|
||||
|
||||
let totalFolderCount: number | undefined;
|
||||
let totalDynamicSecretCount: number | undefined;
|
||||
let totalSecretCount: number | undefined;
|
||||
let totalImportCount: number | undefined;
|
||||
let totalSecretRotationCount: number | undefined;
|
||||
|
||||
if (includeImports) {
|
||||
totalImportCount = await server.services.secretImport.getProjectImportMultiEnvCount({
|
||||
@ -322,6 +347,56 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (includeSecretRotations) {
|
||||
totalSecretRotationCount = await server.services.secretRotationV2.getDashboardSecretRotationCount(
|
||||
{
|
||||
projectId,
|
||||
search,
|
||||
environments,
|
||||
secretPath
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
|
||||
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
|
||||
{
|
||||
projectId,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environments,
|
||||
secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATIONS,
|
||||
metadata: {
|
||||
count: secretRotations.length,
|
||||
rotationIds: secretRotations.map((rotation) => rotation.id),
|
||||
secretPath,
|
||||
environment: environments.join(",")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// get the count of unique secret rotation names to properly adjust remaining limit
|
||||
const uniqueSecretRotationCount = new Set(secretRotations.map((rotation) => rotation.name)).size;
|
||||
|
||||
remainingLimit -= uniqueSecretRotationCount;
|
||||
adjustedOffset = 0;
|
||||
} else {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalSecretRotationCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeSecrets) {
|
||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
||||
@ -353,38 +428,44 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
offset: adjustedOffset,
|
||||
isInternal: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const environment of environments) {
|
||||
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
|
||||
if (secrets?.length || secretRotations?.length) {
|
||||
for await (const environment of environments) {
|
||||
const secretCountFromEnv =
|
||||
(secrets?.filter((secret) => secret.environment === environment).length ?? 0) +
|
||||
(secretRotations
|
||||
?.filter((rotation) => rotation.environment.slug === environment)
|
||||
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0);
|
||||
|
||||
if (secretCountFromEnv) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCountFromEnv
|
||||
}
|
||||
if (secretCountFromEnv) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCountFromEnv
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: secretCountFromEnv,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: secretCountFromEnv,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -395,12 +476,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
dynamicSecrets,
|
||||
secrets,
|
||||
imports,
|
||||
secretRotations,
|
||||
totalFolderCount,
|
||||
totalDynamicSecretCount,
|
||||
totalImportCount,
|
||||
totalSecretCount,
|
||||
totalSecretRotationCount,
|
||||
totalCount:
|
||||
(totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + (totalImportCount ?? 0)
|
||||
(totalFolderCount ?? 0) +
|
||||
(totalDynamicSecretCount ?? 0) +
|
||||
(totalSecretCount ?? 0) +
|
||||
(totalImportCount ?? 0) +
|
||||
(totalSecretRotationCount ?? 0)
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -445,7 +532,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||
includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
|
||||
includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports),
|
||||
includeSecretRotations: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecretRotations)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -457,6 +545,23 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.optional(),
|
||||
folders: SecretFoldersSchema.array().optional(),
|
||||
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
|
||||
secretRotations: z
|
||||
.intersection(
|
||||
SecretRotationV2Schema,
|
||||
z.object({
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
tags: SanitizedTagSchema.array().optional()
|
||||
})
|
||||
.nullable()
|
||||
.array()
|
||||
})
|
||||
)
|
||||
.array()
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
@ -470,6 +575,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalFolderCount: z.number().optional(),
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
totalSecretRotationCount: z.number().optional(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
@ -488,7 +594,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
includeFolders,
|
||||
includeSecrets,
|
||||
includeDynamicSecrets,
|
||||
includeImports
|
||||
includeImports,
|
||||
includeSecretRotations
|
||||
} = req.query;
|
||||
|
||||
if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
|
||||
@ -507,11 +614,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"] | undefined;
|
||||
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
|
||||
let secretRotations:
|
||||
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
|
||||
| undefined;
|
||||
|
||||
let totalImportCount: number | undefined;
|
||||
let totalFolderCount: number | undefined;
|
||||
let totalDynamicSecretCount: number | undefined;
|
||||
let totalSecretCount: number | undefined;
|
||||
let totalSecretRotationCount: number | undefined;
|
||||
|
||||
if (includeImports) {
|
||||
totalImportCount = await server.services.secretImport.getProjectImportCount({
|
||||
@ -594,6 +705,53 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (includeSecretRotations) {
|
||||
totalSecretRotationCount = await server.services.secretRotationV2.getDashboardSecretRotationCount(
|
||||
{
|
||||
projectId,
|
||||
search,
|
||||
environments: [environment],
|
||||
secretPath
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
|
||||
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
|
||||
{
|
||||
projectId,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environments: [environment],
|
||||
secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATIONS,
|
||||
metadata: {
|
||||
count: secretRotations.length,
|
||||
rotationIds: secretRotations.map((rotation) => rotation.id),
|
||||
secretPath,
|
||||
environment
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
remainingLimit -= secretRotations.length;
|
||||
adjustedOffset = 0;
|
||||
} else {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalSecretRotationCount);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (includeDynamicSecrets) {
|
||||
totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({
|
||||
@ -629,7 +787,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!(error instanceof ForbiddenError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (includeSecrets) {
|
||||
totalSecretCount = await server.services.secret.getSecretsCount({
|
||||
actorId: req.permission.id,
|
||||
@ -663,34 +827,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
tagSlugs: tags
|
||||
})
|
||||
).secrets;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secrets.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -699,17 +835,57 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (secrets?.length || secretRotations?.length) {
|
||||
const secretCount =
|
||||
(secrets?.length ?? 0) +
|
||||
(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCount
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: secretCount,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
imports,
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
secrets,
|
||||
secretRotations,
|
||||
totalImportCount,
|
||||
totalFolderCount,
|
||||
totalDynamicSecretCount,
|
||||
totalSecretCount,
|
||||
totalSecretRotationCount,
|
||||
totalCount:
|
||||
(totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
|
||||
(totalImportCount ?? 0) +
|
||||
(totalFolderCount ?? 0) +
|
||||
(totalDynamicSecretCount ?? 0) +
|
||||
(totalSecretCount ?? 0) +
|
||||
(totalSecretRotationCount ?? 0)
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -747,7 +923,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SanitizedTagSchema.array().optional()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
.optional(),
|
||||
secretRotations: SecretRotationV2Schema.array().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -811,6 +988,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission
|
||||
);
|
||||
|
||||
const secretRotations = searchHasTags
|
||||
? []
|
||||
: await server.services.secretRotationV2.getQuickSearchSecretRotations(
|
||||
{
|
||||
projectId,
|
||||
folderMappings,
|
||||
filters: sharedFilters
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
for await (const environment of environments) {
|
||||
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length;
|
||||
|
||||
@ -843,6 +1031,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const secretRotationsFromEnv = secretRotations.filter((rotation) => rotation.environment.slug === environment);
|
||||
|
||||
if (secretRotationsFromEnv.length) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATIONS,
|
||||
metadata: {
|
||||
count: secretRotationsFromEnv.length,
|
||||
rotationIds: secretRotationsFromEnv.map((rotation) => rotation.id),
|
||||
secretPath,
|
||||
environment
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sliceQuickSearch = <T>(array: T[]) => array.slice(0, 25);
|
||||
@ -856,6 +1062,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
? dynamicSecrets.filter((dynamicSecret) => dynamicSecret.path.endsWith(searchPath))
|
||||
: dynamicSecrets
|
||||
),
|
||||
secretRotations: sliceQuickSearch(
|
||||
searchPath ? secretRotations.filter((rotation) => rotation.folder.path.endsWith(searchPath)) : secretRotations
|
||||
),
|
||||
folders: searchHasTags
|
||||
? []
|
||||
: sliceQuickSearch(
|
||||
|
@ -8,15 +8,17 @@ import {
|
||||
ProjectSlackConfigsSchema,
|
||||
ProjectType,
|
||||
SecretFoldersSchema,
|
||||
SortDirection,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
|
||||
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
||||
|
||||
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
@ -704,4 +706,107 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
return environmentsFolders;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/search",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
limit: z.number().default(100),
|
||||
offset: z.number().default(0),
|
||||
type: z.nativeEnum(ProjectType).optional(),
|
||||
orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME),
|
||||
orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val), {
|
||||
message: "Invalid pattern: only alphanumeric characters, - are allowed."
|
||||
})
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
projects: SanitizedProjectSchema.extend({ isMember: z.boolean() }).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { docs: projects, totalCount } = await server.services.project.searchProjects({
|
||||
permission: req.permission,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return { projects, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/project-access",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
comment: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(2500)
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.Comma,
|
||||
CharacterType.Fullstop,
|
||||
CharacterType.Spaces,
|
||||
CharacterType.Exclamation
|
||||
])(val),
|
||||
{
|
||||
message: "Invalid pattern: only alphanumeric characters, spaces, -.!, are allowed."
|
||||
}
|
||||
)
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.project.requestProjectAccess({
|
||||
permission: req.permission,
|
||||
comment: req.body.comment,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
if (req.auth.actor === ActorType.USER) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PROJECT_ACCESS_REQUEST,
|
||||
metadata: {
|
||||
projectId: req.params.workspaceId,
|
||||
requesterEmail: req.auth.user.email || req.auth.user.username,
|
||||
requesterId: req.auth.userId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { message: "Project access request has been send to project admins" };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -8,7 +8,6 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||
|
||||
export * from "./secret-sync-router";
|
||||
|
||||
@ -20,6 +19,5 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
|
||||
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
||||
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter
|
||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter
|
||||
};
|
||||
|
@ -22,7 +22,6 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||
|
||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncSchema,
|
||||
@ -32,8 +31,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AzureKeyVaultSyncSchema,
|
||||
AzureAppConfigurationSyncSchema,
|
||||
DatabricksSyncSchema,
|
||||
HumanitecSyncSchema,
|
||||
TerraformCloudSyncSchema
|
||||
HumanitecSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@ -44,8 +42,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
AzureKeyVaultSyncListItemSchema,
|
||||
AzureAppConfigurationSyncListItemSchema,
|
||||
DatabricksSyncListItemSchema,
|
||||
HumanitecSyncListItemSchema,
|
||||
TerraformCloudSyncListItemSchema
|
||||
HumanitecSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
CreateTerraformCloudSyncSchema,
|
||||
TerraformCloudSyncSchema,
|
||||
UpdateTerraformCloudSyncSchema
|
||||
} from "@app/services/secret-sync/terraform-cloud";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerTerraformCloudSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.TerraformCloud,
|
||||
server,
|
||||
responseSchema: TerraformCloudSyncSchema,
|
||||
createSchema: CreateTerraformCloudSyncSchema,
|
||||
updateSchema: UpdateTerraformCloudSyncSchema
|
||||
});
|
@ -7,6 +7,7 @@ import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -39,13 +40,6 @@ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReference
|
||||
children: z.lazy(() => SecretReferenceNodeTree.array())
|
||||
});
|
||||
|
||||
const BaseSecretNameSchema = z.string().trim().min(1);
|
||||
|
||||
const SecretNameSchema = BaseSecretNameSchema.refine(
|
||||
(el) => !el.includes(" "),
|
||||
"Secret name cannot contain spaces."
|
||||
).refine((el) => !el.includes(":"), "Secret name cannot contain colon.");
|
||||
|
||||
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@ -630,6 +624,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretValue: z
|
||||
.string()
|
||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
||||
.optional()
|
||||
.describe(RAW_SECRETS.UPDATE.secretValue),
|
||||
secretPath: z
|
||||
.string()
|
||||
@ -2049,6 +2044,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretValue: z
|
||||
.string()
|
||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
||||
.optional()
|
||||
.describe(RAW_SECRETS.UPDATE.secretValue),
|
||||
secretPath: z
|
||||
.string()
|
||||
|
@ -6,7 +6,8 @@ export enum AppConnection {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud"
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
@ -1,30 +1,22 @@
|
||||
import { TAppConnections } from "@app/db/schemas/app-connections";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import {
|
||||
AwsConnectionMethod,
|
||||
getAwsConnectionListItem,
|
||||
validateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
DatabricksConnectionMethod,
|
||||
getDatabricksConnectionListItem,
|
||||
validateDatabricksConnectionCredentials
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import {
|
||||
GcpConnectionMethod,
|
||||
getGcpConnectionListItem,
|
||||
validateGcpConnectionCredentials
|
||||
} from "@app/services/app-connection/gcp";
|
||||
import {
|
||||
getGitHubConnectionListItem,
|
||||
GitHubConnectionMethod,
|
||||
validateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
transferSqlConnectionCredentialsToPlatform,
|
||||
validateSqlConnectionCredentials
|
||||
} from "@app/services/app-connection/shared/sql";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { AppConnection } from "./app-connection-enums";
|
||||
import { TAppConnectionServiceFactoryDep } from "./app-connection-service";
|
||||
import {
|
||||
TAppConnection,
|
||||
TAppConnectionConfig,
|
||||
TAppConnectionCredentialsValidator,
|
||||
TAppConnectionTransitionCredentialsToPlatform
|
||||
} from "./app-connection-types";
|
||||
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
getAzureAppConfigurationConnectionListItem,
|
||||
@ -35,16 +27,20 @@ import {
|
||||
getAzureKeyVaultConnectionListItem,
|
||||
validateAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault";
|
||||
import {
|
||||
DatabricksConnectionMethod,
|
||||
getDatabricksConnectionListItem,
|
||||
validateDatabricksConnectionCredentials
|
||||
} from "./databricks";
|
||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||
import {
|
||||
getHumanitecConnectionListItem,
|
||||
HumanitecConnectionMethod,
|
||||
validateHumanitecConnectionCredentials
|
||||
} from "./humanitec";
|
||||
import {
|
||||
getTerraformCloudConnectionListItem,
|
||||
TerraformCloudConnectionMethod,
|
||||
validateTerraformCloudConnectionCredentials
|
||||
} from "./terraform-cloud";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [
|
||||
@ -55,7 +51,8 @@ export const listAppConnectionOptions = () => {
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getDatabricksConnectionListItem(),
|
||||
getHumanitecConnectionListItem(),
|
||||
getTerraformCloudConnectionListItem()
|
||||
getPostgresConnectionListItem(),
|
||||
getMsSqlConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@ -101,32 +98,22 @@ export const decryptAppConnectionCredentials = async ({
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||
};
|
||||
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureAppConfiguration]:
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
export const validateAppConnectionCredentials = async (
|
||||
appConnection: TAppConnectionConfig
|
||||
): Promise<TAppConnection["credentials"]> => {
|
||||
const { app } = appConnection;
|
||||
switch (app) {
|
||||
case AppConnection.AWS:
|
||||
return validateAwsConnectionCredentials(appConnection);
|
||||
case AppConnection.Databricks:
|
||||
return validateDatabricksConnectionCredentials(appConnection);
|
||||
case AppConnection.GitHub:
|
||||
return validateGitHubConnectionCredentials(appConnection);
|
||||
case AppConnection.GCP:
|
||||
return validateGcpConnectionCredentials(appConnection);
|
||||
case AppConnection.AzureKeyVault:
|
||||
return validateAzureKeyVaultConnectionCredentials(appConnection);
|
||||
case AppConnection.AzureAppConfiguration:
|
||||
return validateAzureAppConfigurationConnectionCredentials(appConnection);
|
||||
case AppConnection.Humanitec:
|
||||
return validateHumanitecConnectionCredentials(appConnection);
|
||||
case AppConnection.TerraformCloud:
|
||||
return validateTerraformCloudConnectionCredentials(appConnection);
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection ${app}`);
|
||||
}
|
||||
};
|
||||
): Promise<TAppConnection["credentials"]> => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
|
||||
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||
switch (method) {
|
||||
@ -144,10 +131,11 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
return "Service Account Impersonation";
|
||||
case DatabricksConnectionMethod.ServicePrincipal:
|
||||
return "Service Principal";
|
||||
case HumanitecConnectionMethod.API_TOKEN:
|
||||
return "API Token";
|
||||
case TerraformCloudConnectionMethod.API_TOKEN:
|
||||
case HumanitecConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
@ -168,3 +156,24 @@ export const decryptAppConnection = async (
|
||||
credentialsHash: generateHash(appConnection.encryptedCredentials)
|
||||
} as TAppConnection;
|
||||
};
|
||||
|
||||
const platformManagedCredentialsNotSupported: TAppConnectionTransitionCredentialsToPlatform = ({ app }) => {
|
||||
throw new BadRequestError({
|
||||
message: `${APP_CONNECTION_NAME_MAP[app]} Connections do not support platform managed credentials.`
|
||||
});
|
||||
};
|
||||
|
||||
export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
AppConnection,
|
||||
TAppConnectionTransitionCredentialsToPlatform
|
||||
> = {
|
||||
[AppConnection.AWS]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Databricks]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitHub]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform
|
||||
};
|
||||
|
@ -8,5 +8,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.Databricks]: "Databricks",
|
||||
[AppConnection.Humanitec]: "Humanitec",
|
||||
[AppConnection.TerraformCloud]: "Terraform Cloud"
|
||||
[AppConnection.Postgres]: "PostgreSQL",
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server"
|
||||
};
|
||||
|
@ -3,6 +3,8 @@ import { z } from "zod";
|
||||
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import { TAppConnectionBaseConfig } from "@app/services/app-connection/app-connection-types";
|
||||
|
||||
import { AppConnection } from "./app-connection-enums";
|
||||
|
||||
@ -14,7 +16,10 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||
credentialsHash: z.string().optional()
|
||||
});
|
||||
|
||||
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
export const GenericCreateAppConnectionFieldsSchema = (
|
||||
app: AppConnection,
|
||||
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
|
||||
) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
||||
description: z
|
||||
@ -22,10 +27,16 @@ export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(AppConnections.CREATE(app).description)
|
||||
.describe(AppConnections.CREATE(app).description),
|
||||
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
||||
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
|
||||
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
|
||||
});
|
||||
|
||||
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
export const GenericUpdateAppConnectionFieldsSchema = (
|
||||
app: AppConnection,
|
||||
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
|
||||
) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
||||
description: z
|
||||
@ -33,5 +44,8 @@ export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(AppConnections.UPDATE(app).description)
|
||||
.describe(AppConnections.UPDATE(app).description),
|
||||
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
||||
? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials)
|
||||
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
|
||||
});
|
||||
|
@ -6,25 +6,27 @@ import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
decryptAppConnection,
|
||||
encryptAppConnectionCredentials,
|
||||
getAppConnectionMethodName,
|
||||
listAppConnectionOptions,
|
||||
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
|
||||
validateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-fns";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import {
|
||||
TAppConnection,
|
||||
TAppConnectionConfig,
|
||||
TCreateAppConnectionDTO,
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
import { AppConnection } from "./app-connection-enums";
|
||||
import { APP_CONNECTION_NAME_MAP } from "./app-connection-maps";
|
||||
import {
|
||||
TAppConnection,
|
||||
TAppConnectionConfig,
|
||||
TAppConnectionRaw,
|
||||
TCreateAppConnectionDTO,
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentialsSchema
|
||||
} from "./app-connection-types";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
@ -37,8 +39,8 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
|
||||
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
@ -48,7 +50,7 @@ export type TAppConnectionServiceFactoryDep = {
|
||||
|
||||
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentialsSchema> = {
|
||||
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
|
||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
||||
@ -56,7 +58,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema
|
||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@ -166,20 +169,38 @@ export const appConnectionServiceFactory = ({
|
||||
orgId: actor.orgId
|
||||
} as TAppConnectionConfig);
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
try {
|
||||
const connection = await appConnectionDAL.create({
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
method,
|
||||
app,
|
||||
...params
|
||||
});
|
||||
const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => {
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: connectionCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return appConnectionDAL.create({
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
method,
|
||||
app,
|
||||
...params
|
||||
});
|
||||
};
|
||||
|
||||
let connection: TAppConnectionRaw;
|
||||
|
||||
if (params.isPlatformManagedCredentials) {
|
||||
connection = await TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM[app](
|
||||
{
|
||||
app,
|
||||
orgId: actor.orgId,
|
||||
credentials: validatedCredentials,
|
||||
method
|
||||
} as TAppConnectionConfig,
|
||||
(platformCredentials) => createConnection(platformCredentials)
|
||||
);
|
||||
} else {
|
||||
connection = await createConnection(validatedCredentials);
|
||||
}
|
||||
|
||||
return {
|
||||
...connection,
|
||||
@ -216,11 +237,18 @@ export const appConnectionServiceFactory = ({
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
let encryptedCredentials: undefined | Buffer;
|
||||
// prevent updating credentials or management status if platform managed
|
||||
if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot update credentials or management status for platform managed connections"
|
||||
});
|
||||
}
|
||||
|
||||
let updatedCredentials: undefined | TAppConnection["credentials"];
|
||||
|
||||
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||
|
||||
if (credentials) {
|
||||
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||
|
||||
if (
|
||||
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
||||
method,
|
||||
@ -233,29 +261,53 @@ export const appConnectionServiceFactory = ({
|
||||
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||
});
|
||||
|
||||
const validatedCredentials = await validateAppConnectionCredentials({
|
||||
updatedCredentials = await validateAppConnectionCredentials({
|
||||
app,
|
||||
orgId: actor.orgId,
|
||||
credentials,
|
||||
method
|
||||
} as TAppConnectionConfig);
|
||||
|
||||
if (!validatedCredentials)
|
||||
if (!updatedCredentials)
|
||||
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||
|
||||
encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedConnection = await appConnectionDAL.updateById(connectionId, {
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
...params
|
||||
});
|
||||
const updateConnection = async (connectionCredentials: TAppConnection["credentials"] | undefined) => {
|
||||
const encryptedCredentials = connectionCredentials
|
||||
? await encryptAppConnectionCredentials({
|
||||
credentials: connectionCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return appConnectionDAL.updateById(connectionId, {
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
...params
|
||||
});
|
||||
};
|
||||
|
||||
let updatedConnection: TAppConnectionRaw;
|
||||
|
||||
if (params.isPlatformManagedCredentials) {
|
||||
if (!updatedCredentials)
|
||||
// prevent enabling platform managed credentials without re-confirming credentials
|
||||
throw new BadRequestError({ message: "Credentials required to transition to platform managed credentials" });
|
||||
|
||||
updatedConnection = await TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM[app](
|
||||
{
|
||||
app,
|
||||
orgId: actor.orgId,
|
||||
credentials: updatedCredentials,
|
||||
method
|
||||
} as TAppConnectionConfig,
|
||||
(platformCredentials) => updateConnection(platformCredentials)
|
||||
);
|
||||
} else {
|
||||
updatedConnection = await updateConnection(updatedCredentials);
|
||||
}
|
||||
|
||||
return await decryptAppConnection(updatedConnection, kmsService);
|
||||
} catch (err) {
|
||||
@ -378,7 +430,6 @@ export const appConnectionServiceFactory = ({
|
||||
gcp: gcpConnectionService(connectAppConnectionById),
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
aws: awsConnectionService(connectAppConnectionById),
|
||||
humanitec: humanitecConnectionService(connectAppConnectionById),
|
||||
terraformCloud: terraformCloudConnectionService(connectAppConnectionById)
|
||||
humanitec: humanitecConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
@ -1,49 +1,56 @@
|
||||
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { AWSRegion } from "./app-connection-enums";
|
||||
import {
|
||||
TAwsConnection,
|
||||
TAwsConnectionConfig,
|
||||
TAwsConnectionInput,
|
||||
TValidateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
TDatabricksConnection,
|
||||
TDatabricksConnectionConfig,
|
||||
TDatabricksConnectionInput,
|
||||
TValidateDatabricksConnectionCredentials
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import {
|
||||
TGitHubConnection,
|
||||
TGitHubConnectionConfig,
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
TValidateAwsConnectionCredentialsSchema
|
||||
} from "./aws";
|
||||
import {
|
||||
TAzureAppConfigurationConnection,
|
||||
TAzureAppConfigurationConnectionConfig,
|
||||
TAzureAppConfigurationConnectionInput,
|
||||
TValidateAzureAppConfigurationConnectionCredentials
|
||||
TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
} from "./azure-app-configuration";
|
||||
import {
|
||||
TAzureKeyVaultConnection,
|
||||
TAzureKeyVaultConnectionConfig,
|
||||
TAzureKeyVaultConnectionInput,
|
||||
TValidateAzureKeyVaultConnectionCredentials
|
||||
TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||
} from "./azure-key-vault";
|
||||
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
|
||||
import {
|
||||
TDatabricksConnection,
|
||||
TDatabricksConnectionConfig,
|
||||
TDatabricksConnectionInput,
|
||||
TValidateDatabricksConnectionCredentialsSchema
|
||||
} from "./databricks";
|
||||
import {
|
||||
TGcpConnection,
|
||||
TGcpConnectionConfig,
|
||||
TGcpConnectionInput,
|
||||
TValidateGcpConnectionCredentialsSchema
|
||||
} from "./gcp";
|
||||
import {
|
||||
TGitHubConnection,
|
||||
TGitHubConnectionConfig,
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentialsSchema
|
||||
} from "./github";
|
||||
import {
|
||||
THumanitecConnection,
|
||||
THumanitecConnectionConfig,
|
||||
THumanitecConnectionInput,
|
||||
TValidateHumanitecConnectionCredentials
|
||||
TValidateHumanitecConnectionCredentialsSchema
|
||||
} from "./humanitec";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import {
|
||||
TTerraformCloudConnection,
|
||||
TTerraformCloudConnectionConfig,
|
||||
TTerraformCloudConnectionInput,
|
||||
TValidateTerraformCloudConnectionCredentials
|
||||
} from "./terraform-cloud";
|
||||
TPostgresConnection,
|
||||
TPostgresConnectionInput,
|
||||
TValidatePostgresConnectionCredentialsSchema
|
||||
} from "./postgres";
|
||||
|
||||
export type TAppConnection = { id: string } & (
|
||||
| TAwsConnection
|
||||
@ -53,9 +60,14 @@ export type TAppConnection = { id: string } & (
|
||||
| TAzureAppConfigurationConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TTerraformCloudConnection
|
||||
| TPostgresConnection
|
||||
| TMsSqlConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
|
||||
export type TSqlConnection = TPostgresConnection | TMsSqlConnection;
|
||||
|
||||
export type TAppConnectionInput = { id: string } & (
|
||||
| TAwsConnectionInput
|
||||
| TGitHubConnectionInput
|
||||
@ -64,12 +76,15 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TAzureAppConfigurationConnectionInput
|
||||
| TDatabricksConnectionInput
|
||||
| THumanitecConnectionInput
|
||||
| TTerraformCloudConnectionInput
|
||||
| TPostgresConnectionInput
|
||||
| TMsSqlConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnectionInput,
|
||||
"credentials" | "method" | "name" | "app" | "description"
|
||||
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials"
|
||||
>;
|
||||
|
||||
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
||||
@ -84,20 +99,34 @@ export type TAppConnectionConfig =
|
||||
| TAzureAppConfigurationConnectionConfig
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
| TTerraformCloudConnectionConfig;
|
||||
| TSqlConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentials =
|
||||
| TValidateAwsConnectionCredentials
|
||||
| TValidateGitHubConnectionCredentials
|
||||
| TValidateGcpConnectionCredentials
|
||||
| TValidateAzureKeyVaultConnectionCredentials
|
||||
| TValidateAzureAppConfigurationConnectionCredentials
|
||||
| TValidateDatabricksConnectionCredentials
|
||||
| TValidateHumanitecConnectionCredentials
|
||||
| TValidateTerraformCloudConnectionCredentials;
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
| TValidateGitHubConnectionCredentialsSchema
|
||||
| TValidateGcpConnectionCredentialsSchema
|
||||
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
| TValidateDatabricksConnectionCredentialsSchema
|
||||
| TValidateHumanitecConnectionCredentialsSchema
|
||||
| TValidatePostgresConnectionCredentialsSchema
|
||||
| TValidateMsSqlConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
region: AWSRegion;
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
||||
export type TAppConnectionCredentialsValidator = (
|
||||
appConnection: TAppConnectionConfig
|
||||
) => Promise<TAppConnection["credentials"]>;
|
||||
|
||||
export type TAppConnectionTransitionCredentialsToPlatform = (
|
||||
appConnection: TAppConnectionConfig,
|
||||
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>
|
||||
) => Promise<TAppConnectionRaw>;
|
||||
|
||||
export type TAppConnectionBaseConfig = {
|
||||
supportsPlatformManagedCredentials?: boolean;
|
||||
};
|
||||
|
@ -92,7 +92,7 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
|
||||
resp = await sts.getCallerIdentity().promise();
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -48,11 +48,11 @@ export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
|
||||
|
||||
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections.CREATE(AppConnection.AWS).method),
|
||||
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(AppConnections.CREATE(AppConnection.AWS).credentials)
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.AWS).method),
|
||||
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AWS).credentials
|
||||
)
|
||||
|
@ -15,7 +15,7 @@ export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
|
||||
app: AppConnection.AWS;
|
||||
};
|
||||
|
||||
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema;
|
||||
export type TValidateAwsConnectionCredentialsSchema = typeof ValidateAwsConnectionCredentialsSchema;
|
||||
|
||||
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
|
@ -57,7 +57,7 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export type TAzureAppConfigurationConnectionInput = z.infer<typeof CreateAzureAp
|
||||
app: AppConnection.AzureAppConfiguration;
|
||||
};
|
||||
|
||||
export type TValidateAzureAppConfigurationConnectionCredentials =
|
||||
export type TValidateAzureAppConfigurationConnectionCredentialsSchema =
|
||||
typeof ValidateAzureAppConfigurationConnectionCredentialsSchema;
|
||||
|
||||
export type TAzureAppConfigurationConnectionConfig = DiscriminativePick<
|
||||
|
@ -129,7 +129,7 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export type TAzureKeyVaultConnectionInput = z.infer<typeof CreateAzureKeyVaultCo
|
||||
app: AppConnection.AzureKeyVault;
|
||||
};
|
||||
|
||||
export type TValidateAzureKeyVaultConnectionCredentials = typeof ValidateAzureKeyVaultConnectionCredentialsSchema;
|
||||
export type TValidateAzureKeyVaultConnectionCredentialsSchema = typeof ValidateAzureKeyVaultConnectionCredentialsSchema;
|
||||
|
||||
export type TAzureKeyVaultConnectionConfig = DiscriminativePick<
|
||||
TAzureKeyVaultConnectionInput,
|
||||
|
@ -86,7 +86,7 @@ export const validateDatabricksConnectionCredentials = async (appConnection: TDa
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -49,7 +49,7 @@ export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnio
|
||||
z.object({
|
||||
method: z
|
||||
.literal(DatabricksConnectionMethod.ServicePrincipal)
|
||||
.describe(AppConnections?.CREATE(AppConnection.Databricks).method),
|
||||
.describe(AppConnections.CREATE(AppConnection.Databricks).method),
|
||||
credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Databricks).credentials
|
||||
)
|
||||
|
@ -15,7 +15,7 @@ export type TDatabricksConnectionInput = z.infer<typeof CreateDatabricksConnecti
|
||||
app: AppConnection.Databricks;
|
||||
};
|
||||
|
||||
export type TValidateDatabricksConnectionCredentials = typeof ValidateDatabricksConnectionCredentialsSchema;
|
||||
export type TValidateDatabricksConnectionCredentialsSchema = typeof ValidateDatabricksConnectionCredentialsSchema;
|
||||
|
||||
export type TDatabricksConnectionConfig = DiscriminativePick<
|
||||
TDatabricksConnection,
|
||||
|
@ -4,10 +4,10 @@ import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { getAppConnectionMethodName } from "../app-connection-fns";
|
||||
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||
import {
|
||||
GCPApp,
|
||||
|
@ -37,7 +37,7 @@ export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("meth
|
||||
z.object({
|
||||
method: z
|
||||
.literal(GcpConnectionMethod.ServiceAccountImpersonation)
|
||||
.describe(AppConnections?.CREATE(AppConnection.GCP).method),
|
||||
.describe(AppConnections.CREATE(AppConnection.GCP).method),
|
||||
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.GCP).credentials
|
||||
)
|
||||
|
@ -15,7 +15,7 @@ export type TGcpConnectionInput = z.infer<typeof CreateGcpConnectionSchema> & {
|
||||
app: AppConnection.GCP;
|
||||
};
|
||||
|
||||
export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema;
|
||||
export type TValidateGcpConnectionCredentialsSchema = typeof ValidateGcpConnectionCredentialsSchema;
|
||||
|
||||
export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
|
@ -200,7 +200,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,6 @@ export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema
|
||||
app: AppConnection.GitHub;
|
||||
};
|
||||
|
||||
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
|
||||
export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema;
|
||||
|
||||
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user