mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
238 Commits
misc/final
...
testing-me
Author | SHA1 | Date | |
---|---|---|---|
0b1f4f0e2a | |||
b4485a2a57 | |||
df50e3b0f9 | |||
bdf2ae40b6 | |||
960efb9cf9 | |||
aa8d58abad | |||
cfb0cc4fea | |||
7712df296c | |||
7c38932121 | |||
69ad9845e1 | |||
7321c237d7 | |||
32430a6a16 | |||
f034adba76 | |||
463eb0014e | |||
21403f6fe5 | |||
2f9e542b31 | |||
089d6812fd | |||
71c9c0fa1e | |||
2b977eeb33 | |||
a692148597 | |||
64bfa4f334 | |||
e3eb14bfd9 | |||
24b50651c9 | |||
1cd459fda7 | |||
38917327d9 | |||
d7b494c6f8 | |||
93208afb36 | |||
1a084d8fcf | |||
dd4f133c6c | |||
c41d27e1ae | |||
1866ed8d23 | |||
7b3b232dde | |||
9d618b4ae9 | |||
5330ab2171 | |||
662e588c22 | |||
90057d80ff | |||
1eda7aaaac | |||
00dcadbc08 | |||
7a7289ebd0 | |||
e5d4677fd6 | |||
bce3f3d676 | |||
300372fa98 | |||
47a4f8bae9 | |||
863719f296 | |||
7317dc1cf5 | |||
75df898e78 | |||
0de6add3f7 | |||
0c008b6393 | |||
0c3894496c | |||
35fbd5d49d | |||
d03b453e3d | |||
96e331b678 | |||
d4d468660d | |||
75a4965928 | |||
660c09ded4 | |||
b5287d91c0 | |||
6a17763237 | |||
f2bd3daea2 | |||
7f70f96936 | |||
73e0a54518 | |||
0d295a2824 | |||
9a62efea4f | |||
506c30bcdb | |||
735ad4ff65 | |||
41e36dfcef | |||
421d8578b7 | |||
6685f8aa0a | |||
d6c37c1065 | |||
54f3f94185 | |||
907537f7c0 | |||
61263b9384 | |||
d71c85e052 | |||
b6d8be2105 | |||
0693f81d0a | |||
61d516ef35 | |||
31fc64fb4c | |||
8bf7e4c4d1 | |||
2027d4b44e | |||
d401c9074e | |||
afe35dbbb5 | |||
6ff1602fd5 | |||
6603364749 | |||
53bea22b85 | |||
7c84adc1c2 | |||
fa8d6735a1 | |||
a6137f267d | |||
d521ee7b7e | |||
827931e416 | |||
faa83344a7 | |||
3be3d807d2 | |||
9f7ea3c4e5 | |||
e67218f170 | |||
269c40c67c | |||
089a7e880b | |||
64ec741f1a | |||
c98233ddaf | |||
ae17981c41 | |||
6c49c7da3c | |||
2de04b6fe5 | |||
5c9ec1e4be | |||
ba89491d4c | |||
483e596a7a | |||
65f122bd41 | |||
682b552fdc | |||
d4cfd0b6ed | |||
ba1fd8a3f7 | |||
e8f09d2c7b | |||
774371a218 | |||
c4b54de303 | |||
433971a72d | |||
4acf9413f0 | |||
f0549cab98 | |||
d75e49dce5 | |||
8819abd710 | |||
796f76da46 | |||
d6e1ed4d1e | |||
1295b68d80 | |||
c79f84c064 | |||
d0c50960ef | |||
85089a08e1 | |||
bf97294dad | |||
4053078d95 | |||
4ba3899861 | |||
6bae3628c0 | |||
4cb935dae7 | |||
ccad684ab2 | |||
fd77708cad | |||
9aebd712d1 | |||
05f07b25ac | |||
5b0dbf04b2 | |||
b050db84ab | |||
8fef6911f1 | |||
44ba31a743 | |||
6bdbac4750 | |||
60fb195706 | |||
c8109b4e84 | |||
1f2b0443cc | |||
dd1cabf9f6 | |||
8b781b925a | |||
ddcf5b576b | |||
7138b392f2 | |||
bfce1021fb | |||
93c0313b28 | |||
8cfc217519 | |||
d272c6217a | |||
2fe2ddd9fc | |||
e330ddd5ee | |||
7aba9c1a50 | |||
4cd8e0fa67 | |||
ea3d164ead | |||
df468e4865 | |||
66e96018c4 | |||
3b02eedca6 | |||
a55fe2b788 | |||
5d7a267f1d | |||
b16ab6f763 | |||
2d2ad0724f | |||
e90efb7fc8 | |||
17d5e4bdab | |||
f22a5580a6 | |||
334a728259 | |||
4a3143e689 | |||
14810de054 | |||
8cfcbaa12c | |||
0e946f73bd | |||
7b8551f883 | |||
3b1ce86ee6 | |||
c649661133 | |||
70e44d04ef | |||
0dddd58be1 | |||
148f522c58 | |||
d4c911a28f | |||
603fcd8ab5 | |||
a1474145ae | |||
7c055f71f7 | |||
14884cd6b0 | |||
98fd146e85 | |||
1d3dca11e7 | |||
22f8a3daa7 | |||
395b3d9e05 | |||
1041e136fb | |||
21024b0d72 | |||
00e68dc0bf | |||
5e068cd8a0 | |||
abdf8f46a3 | |||
1cf046f6b3 | |||
0fda6d6f4d | |||
8d4115925c | |||
d0b3c6b66a | |||
a1685af119 | |||
8d4a06e9e4 | |||
6dbe3c8793 | |||
a3ec1a27de | |||
472f02e8b1 | |||
3989646b80 | |||
472f5eb8b4 | |||
f5b039f939 | |||
b7b3d07e9f | |||
891a1ea2b9 | |||
a807f0cf6c | |||
cfc0b2fb8d | |||
f096a567de | |||
65d642113d | |||
92e7e90c21 | |||
f9f6ec0a8d | |||
d9621b0b17 | |||
d80a70731d | |||
bd99b4e356 | |||
7db0bd7daa | |||
8bc538af93 | |||
8ef078872e | |||
d5f718c6ad | |||
5f93016d22 | |||
f220246eb4 | |||
829b399cda | |||
3f6a0c77f1 | |||
9e4b66e215 | |||
8a14914bc3 | |||
829ae7d3c0 | |||
19c26c680c | |||
027b200b1a | |||
e761e65322 | |||
370ed45abb | |||
61f786e8d8 | |||
26064e3a08 | |||
9b246166a1 | |||
6956d14e2e | |||
bae7c6c3d7 | |||
e8b33f27fc | |||
ac0cb6d96f | |||
f71f894de8 | |||
66d2cc8947 | |||
e034aa381a | |||
fc3a409164 | |||
ffc58b0313 | |||
9a7e05369c | |||
33b49f4466 | |||
60895537a7 |
10
.env.example
10
.env.example
@ -74,9 +74,17 @@ CAPTCHA_SECRET=
|
|||||||
|
|
||||||
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||||
|
|
||||||
|
OTEL_TELEMETRY_COLLECTION_ENABLED=
|
||||||
|
OTEL_EXPORT_TYPE=
|
||||||
|
OTEL_EXPORT_OTLP_ENDPOINT=
|
||||||
|
OTEL_OTLP_PUSH_INTERVAL=
|
||||||
|
|
||||||
|
OTEL_COLLECTOR_BASIC_AUTH_USERNAME=
|
||||||
|
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=
|
||||||
|
|
||||||
PLAIN_API_KEY=
|
PLAIN_API_KEY=
|
||||||
PLAIN_WISH_LABEL_IDS=
|
PLAIN_WISH_LABEL_IDS=
|
||||||
|
|
||||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||||
|
|
||||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=
|
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
||||||
|
@ -1,62 +1,115 @@
|
|||||||
name: Release standalone docker image
|
name: Release standalone docker image
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "infisical/v*.*.*-postgres"
|
- "infisical/v*.*.*-postgres"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
infisical-tests:
|
infisical-tests:
|
||||||
name: Run tests before deployment
|
name: Run tests before deployment
|
||||||
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
|
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
|
||||||
uses: ./.github/workflows/run-backend-tests.yml
|
uses: ./.github/workflows/run-backend-tests.yml
|
||||||
infisical-standalone:
|
|
||||||
name: Build infisical standalone image postgres
|
infisical-standalone:
|
||||||
runs-on: ubuntu-latest
|
name: Build infisical standalone image postgres
|
||||||
needs: [infisical-tests]
|
runs-on: ubuntu-latest
|
||||||
steps:
|
needs: [infisical-tests]
|
||||||
- name: Extract version from tag
|
steps:
|
||||||
id: extract_version
|
- name: Extract version from tag
|
||||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
id: extract_version
|
||||||
- name: ☁️ Checkout source
|
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||||
uses: actions/checkout@v3
|
- name: ☁️ Checkout source
|
||||||
with:
|
uses: actions/checkout@v3
|
||||||
fetch-depth: 0
|
with:
|
||||||
- name: 📦 Install dependencies to test all dependencies
|
fetch-depth: 0
|
||||||
run: npm ci --only-production
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
working-directory: backend
|
run: npm ci --only-production
|
||||||
- name: version output
|
working-directory: backend
|
||||||
run: |
|
- name: version output
|
||||||
echo "Output Value: ${{ steps.version.outputs.major }}"
|
run: |
|
||||||
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
echo "Output Value: ${{ steps.version.outputs.major }}"
|
||||||
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
||||||
echo "Output Value: ${{ steps.version.outputs.version }}"
|
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
||||||
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
echo "Output Value: ${{ steps.version.outputs.version }}"
|
||||||
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
||||||
- name: Save commit hashes for tag
|
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
||||||
id: commit
|
- name: Save commit hashes for tag
|
||||||
uses: pr-mpt/actions-commit-hash@v2
|
id: commit
|
||||||
- name: 🔧 Set up Docker Buildx
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
uses: docker/setup-buildx-action@v2
|
- name: 🔧 Set up Docker Buildx
|
||||||
- name: 🐋 Login to Docker Hub
|
uses: docker/setup-buildx-action@v2
|
||||||
uses: docker/login-action@v2
|
- name: 🐋 Login to Docker Hub
|
||||||
with:
|
uses: docker/login-action@v2
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
with:
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
- name: Set up Depot CLI
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
uses: depot/setup-action@v1
|
- name: Set up Depot CLI
|
||||||
- name: 📦 Build backend and export to Docker
|
uses: depot/setup-action@v1
|
||||||
uses: depot/build-push-action@v1
|
- name: 📦 Build backend and export to Docker
|
||||||
with:
|
uses: depot/build-push-action@v1
|
||||||
project: 64mmf0n610
|
with:
|
||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
project: 64mmf0n610
|
||||||
push: true
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
context: .
|
push: true
|
||||||
tags: |
|
context: .
|
||||||
infisical/infisical:latest-postgres
|
tags: |
|
||||||
infisical/infisical:${{ steps.commit.outputs.short }}
|
infisical/infisical:latest-postgres
|
||||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||||
platforms: linux/amd64,linux/arm64
|
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||||
file: Dockerfile.standalone-infisical
|
platforms: linux/amd64,linux/arm64
|
||||||
build-args: |
|
file: Dockerfile.standalone-infisical
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
build-args: |
|
||||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||||
|
|
||||||
|
infisical-fips-standalone:
|
||||||
|
name: Build infisical standalone image postgres
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [infisical-tests]
|
||||||
|
steps:
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: extract_version
|
||||||
|
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
|
run: npm ci --only-production
|
||||||
|
working-directory: backend
|
||||||
|
- name: version output
|
||||||
|
run: |
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.major }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.version }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
- name: 📦 Build backend and export to Docker
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
push: true
|
||||||
|
context: .
|
||||||
|
tags: |
|
||||||
|
infisical/infisical-fips:latest-postgres
|
||||||
|
infisical/infisical-fips:${{ steps.commit.outputs.short }}
|
||||||
|
infisical/infisical-fips:${{ steps.extract_version.outputs.version }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
file: Dockerfile.fips.standalone-infisical
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||||
|
@ -10,8 +10,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
# packages: write
|
|
||||||
# issues: write
|
|
||||||
jobs:
|
jobs:
|
||||||
cli-integration-tests:
|
cli-integration-tests:
|
||||||
name: Run tests before deployment
|
name: Run tests before deployment
|
||||||
@ -26,6 +25,63 @@ jobs:
|
|||||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||||
|
|
||||||
|
npm-release:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
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:
|
||||||
|
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:
|
goreleaser:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [cli-integration-tests]
|
needs: [cli-integration-tests]
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -71,3 +71,5 @@ frontend-build
|
|||||||
cli/infisical-merge
|
cli/infisical-merge
|
||||||
cli/test/infisical-merge
|
cli/test/infisical-merge
|
||||||
/backend/binary
|
/backend/binary
|
||||||
|
|
||||||
|
/npm/bin
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
# Check if infisical is installed
|
||||||
|
if ! command -v infisical >/dev/null 2>&1; then
|
||||||
|
echo "\nError: Infisical CLI is not installed. Please install the Infisical CLI before comitting.\n You can refer to the documentation at https://infisical.com/docs/cli/overview\n\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
npx lint-staged
|
npx lint-staged
|
||||||
|
|
||||||
infisical scan git-changes --staged -v
|
infisical scan git-changes --staged -v
|
||||||
|
@ -6,3 +6,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S
|
|||||||
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
|
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
|
||||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
|
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
|
||||||
docs/mint.json:generic-api-key:651
|
docs/mint.json:generic-api-key:651
|
||||||
|
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134
|
||||||
|
194
Dockerfile.fips.standalone-infisical
Normal file
194
Dockerfile.fips.standalone-infisical
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
ARG POSTHOG_HOST=https://app.posthog.com
|
||||||
|
ARG POSTHOG_API_KEY=posthog-api-key
|
||||||
|
ARG INTERCOM_ID=intercom-id
|
||||||
|
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||||
|
|
||||||
|
FROM node:20-slim AS base
|
||||||
|
|
||||||
|
FROM base AS frontend-dependencies
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only-production --ignore-scripts
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS frontend-builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies
|
||||||
|
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||||
|
# Copy all files
|
||||||
|
COPY /frontend .
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV NEXT_PUBLIC_ENV production
|
||||||
|
ARG POSTHOG_HOST
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
||||||
|
ARG POSTHOG_API_KEY
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||||
|
ARG INTERCOM_ID
|
||||||
|
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||||
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
|
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
|
ARG CAPTCHA_SITE_KEY
|
||||||
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
|
# Build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production image
|
||||||
|
FROM base AS frontend-runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|
||||||
|
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
|
||||||
|
VOLUME /app/.next/cache/images
|
||||||
|
|
||||||
|
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
||||||
|
COPY --from=frontend-builder /app/public ./public
|
||||||
|
RUN chown non-root-user:nodejs ./public/data
|
||||||
|
|
||||||
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER non-root-user
|
||||||
|
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
##
|
||||||
|
## BACKEND
|
||||||
|
##
|
||||||
|
FROM base AS backend-build
|
||||||
|
|
||||||
|
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||||
|
|
||||||
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Required for pkcs11js and ODBC
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
unixodbc-dev \
|
||||||
|
freetds-dev \
|
||||||
|
freetds-bin \
|
||||||
|
tdsodbc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Configure ODBC
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
|
COPY backend/package*.json ./
|
||||||
|
RUN npm ci --only-production
|
||||||
|
|
||||||
|
COPY /backend .
|
||||||
|
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
|
||||||
|
RUN npm i -D tsconfig-paths
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM base AS backend-runner
|
||||||
|
|
||||||
|
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Required for pkcs11js and ODBC
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
unixodbc-dev \
|
||||||
|
freetds-dev \
|
||||||
|
freetds-bin \
|
||||||
|
tdsodbc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Configure ODBC
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
|
COPY backend/package*.json ./
|
||||||
|
RUN npm ci --only-production
|
||||||
|
|
||||||
|
COPY --from=backend-build /app .
|
||||||
|
|
||||||
|
RUN mkdir frontend-build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM base AS production
|
||||||
|
|
||||||
|
# Install necessary packages including ODBC
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
unixodbc-dev \
|
||||||
|
freetds-dev \
|
||||||
|
freetds-bin \
|
||||||
|
tdsodbc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Configure ODBC in production
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
|
# 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.31.1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|
||||||
|
# Give non-root-user permission to update SSL certs
|
||||||
|
RUN chown -R non-root-user /etc/ssl/certs
|
||||||
|
RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt
|
||||||
|
RUN chmod -R u+rwx /etc/ssl/certs
|
||||||
|
RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt
|
||||||
|
RUN chown non-root-user /usr/sbin/update-ca-certificates
|
||||||
|
RUN chmod u+rx /usr/sbin/update-ca-certificates
|
||||||
|
|
||||||
|
## set pre baked keys
|
||||||
|
ARG POSTHOG_API_KEY
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||||
|
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||||
|
ARG INTERCOM_ID=intercom-id
|
||||||
|
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||||
|
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||||
|
ARG CAPTCHA_SITE_KEY
|
||||||
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
||||||
|
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=backend-runner /app /backend
|
||||||
|
|
||||||
|
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||||
|
|
||||||
|
ENV PORT 8080
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV HTTPS_ENABLED false
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV STANDALONE_BUILD true
|
||||||
|
ENV STANDALONE_MODE true
|
||||||
|
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||||
|
|
||||||
|
WORKDIR /backend
|
||||||
|
|
||||||
|
ENV TELEMETRY_ENABLED true
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
USER non-root-user
|
||||||
|
|
||||||
|
CMD ["./standalone-entrypoint.sh"]
|
@ -72,6 +72,17 @@ RUN addgroup --system --gid 1001 nodejs \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install all required dependencies for build
|
||||||
|
RUN apk --update add \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
freetds \
|
||||||
|
unixodbc-dev \
|
||||||
|
libc-dev \
|
||||||
|
freetds-dev
|
||||||
|
|
||||||
COPY backend/package*.json ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
|
|
||||||
@ -85,6 +96,20 @@ FROM base AS backend-runner
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install all required dependencies for runtime
|
||||||
|
RUN apk --update add \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
freetds \
|
||||||
|
unixodbc-dev \
|
||||||
|
libc-dev \
|
||||||
|
freetds-dev
|
||||||
|
|
||||||
|
# Configure ODBC
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
COPY backend/package*.json ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
|
|
||||||
@ -94,11 +119,32 @@ RUN mkdir frontend-build
|
|||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
|
|
||||||
RUN apk add --upgrade --no-cache ca-certificates
|
RUN apk add --upgrade --no-cache ca-certificates
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||||
&& apk add infisical=0.31.1 && apk add --no-cache git
|
&& apk add infisical=0.31.1 && apk add --no-cache git
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
# Install all required runtime dependencies
|
||||||
|
RUN apk --update add \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
freetds \
|
||||||
|
unixodbc-dev \
|
||||||
|
libc-dev \
|
||||||
|
freetds-dev \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
git
|
||||||
|
|
||||||
|
# Configure ODBC in production
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
|
# Setup user permissions
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 non-root-user
|
&& adduser --system --uid 1001 non-root-user
|
||||||
|
|
||||||
@ -121,7 +167,6 @@ ARG CAPTCHA_SITE_KEY
|
|||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
||||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
COPY --from=backend-runner /app /backend
|
COPY --from=backend-runner /app /backend
|
||||||
|
|
||||||
@ -143,4 +188,4 @@ EXPOSE 443
|
|||||||
|
|
||||||
USER non-root-user
|
USER non-root-user
|
||||||
|
|
||||||
CMD ["./standalone-entrypoint.sh"]
|
CMD ["./standalone-entrypoint.sh"]
|
4
Makefile
4
Makefile
@ -10,6 +10,9 @@ up-dev:
|
|||||||
up-dev-ldap:
|
up-dev-ldap:
|
||||||
docker compose -f docker-compose.dev.yml --profile ldap up --build
|
docker compose -f docker-compose.dev.yml --profile ldap up --build
|
||||||
|
|
||||||
|
up-dev-metrics:
|
||||||
|
docker compose -f docker-compose.dev.yml --profile metrics up --build
|
||||||
|
|
||||||
up-prod:
|
up-prod:
|
||||||
docker-compose -f docker-compose.prod.yml up --build
|
docker-compose -f docker-compose.prod.yml up --build
|
||||||
|
|
||||||
@ -27,4 +30,3 @@ reviewable-api:
|
|||||||
npm run type:check
|
npm run type:check
|
||||||
|
|
||||||
reviewable: reviewable-ui reviewable-api
|
reviewable: reviewable-ui reviewable-api
|
||||||
|
|
||||||
|
@ -3,6 +3,21 @@ FROM node:20-alpine AS build
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Required for pkcs11js
|
||||||
|
RUN apk --update add \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++
|
||||||
|
|
||||||
|
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
unixodbc \
|
||||||
|
freetds \
|
||||||
|
unixodbc-dev \
|
||||||
|
libc-dev \
|
||||||
|
freetds-dev
|
||||||
|
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
|
|
||||||
@ -11,12 +26,28 @@ RUN npm run build
|
|||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV npm_config_cache /home/node/.npm
|
ENV npm_config_cache /home/node/.npm
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN apk --update add \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++
|
||||||
|
|
||||||
|
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
unixodbc \
|
||||||
|
freetds \
|
||||||
|
unixodbc-dev \
|
||||||
|
libc-dev \
|
||||||
|
freetds-dev
|
||||||
|
|
||||||
|
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
RUN npm ci --only-production && npm cache clean --force
|
RUN npm ci --only-production && npm cache clean --force
|
||||||
|
|
||||||
COPY --from=build /app .
|
COPY --from=build /app .
|
||||||
|
@ -1,5 +1,56 @@
|
|||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# ? 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 apk --update add \
|
||||||
|
alpine-sdk \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
git \
|
||||||
|
libtool \
|
||||||
|
openssl-dev \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++
|
||||||
|
|
||||||
|
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
unixodbc \
|
||||||
|
freetds \
|
||||||
|
unixodbc-dev \
|
||||||
|
libc-dev \
|
||||||
|
freetds-dev
|
||||||
|
|
||||||
|
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/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 apk --update add opensc
|
||||||
|
|
||||||
|
RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
||||||
|
|
||||||
|
# ? App setup
|
||||||
|
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
&& apk add infisical=0.8.1 && apk add --no-cache git
|
||||||
|
@ -5,6 +5,9 @@ export const mockSmtpServer = (): TSmtpService => {
|
|||||||
return {
|
return {
|
||||||
sendMail: async (data) => {
|
sendMail: async (data) => {
|
||||||
storage.push(data);
|
storage.push(data);
|
||||||
|
},
|
||||||
|
verify: async () => {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ import { initDbConnection } from "@app/db";
|
|||||||
import { queueServiceFactory } from "@app/queue";
|
import { queueServiceFactory } from "@app/queue";
|
||||||
import { keyStoreFactory } from "@app/keystore/keystore";
|
import { keyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||||
|
|
||||||
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
||||||
export default {
|
export default {
|
||||||
@ -54,7 +55,12 @@ export default {
|
|||||||
const smtp = mockSmtpServer();
|
const smtp = mockSmtpServer();
|
||||||
const queue = queueServiceFactory(cfg.REDIS_URL);
|
const queue = queueServiceFactory(cfg.REDIS_URL);
|
||||||
const keyStore = keyStoreFactory(cfg.REDIS_URL);
|
const keyStore = keyStoreFactory(cfg.REDIS_URL);
|
||||||
const server = await main({ db, smtp, logger, queue, keyStore });
|
|
||||||
|
const hsmModule = initializeHsmModule();
|
||||||
|
hsmModule.initialize();
|
||||||
|
|
||||||
|
const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule() });
|
||||||
|
|
||||||
// @ts-expect-error type
|
// @ts-expect-error type
|
||||||
globalThis.testServer = server;
|
globalThis.testServer = server;
|
||||||
// @ts-expect-error type
|
// @ts-expect-error type
|
||||||
|
1733
backend/package-lock.json
generated
1733
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -50,6 +50,7 @@
|
|||||||
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
|
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
|
||||||
"auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list",
|
"auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list",
|
||||||
"auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status",
|
"auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status",
|
||||||
|
"auditlog-migration:unlock": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:unlock",
|
||||||
"auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback",
|
"auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback",
|
||||||
"migration:new": "tsx ./scripts/create-migration.ts",
|
"migration:new": "tsx ./scripts/create-migration.ts",
|
||||||
"migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
"migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||||
@ -58,6 +59,7 @@
|
|||||||
"migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
"migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||||
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||||
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||||
|
"migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./src/db/knexfile.ts migrate:unlock",
|
||||||
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
||||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||||
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||||
@ -84,6 +86,7 @@
|
|||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
|
"@types/pkcs11js": "^1.0.4",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
@ -129,6 +132,7 @@
|
|||||||
"@fastify/multipart": "8.3.0",
|
"@fastify/multipart": "8.3.0",
|
||||||
"@fastify/passport": "^2.4.0",
|
"@fastify/passport": "^2.4.0",
|
||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
@ -137,6 +141,14 @@
|
|||||||
"@octokit/plugin-retry": "^5.0.5",
|
"@octokit/plugin-retry": "^5.0.5",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
|
||||||
|
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
|
||||||
|
"@opentelemetry/exporter-prometheus": "^0.55.0",
|
||||||
|
"@opentelemetry/instrumentation": "^0.55.0",
|
||||||
|
"@opentelemetry/resources": "^1.28.0",
|
||||||
|
"@opentelemetry/sdk-metrics": "^1.28.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@peculiar/x509": "^1.12.1",
|
"@peculiar/x509": "^1.12.1",
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
@ -177,9 +189,11 @@
|
|||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
|
"odbc": "^2.4.9",
|
||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
@ -188,6 +202,7 @@
|
|||||||
"pg-query-stream": "^4.5.3",
|
"pg-query-stream": "^4.5.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
|
"pkcs11js": "^2.1.6",
|
||||||
"pkijs": "^3.2.4",
|
"pkijs": "^3.2.4",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.3.8",
|
"probot": "^13.3.8",
|
||||||
|
7
backend/src/@types/fastify-request-context.d.ts
vendored
Normal file
7
backend/src/@types/fastify-request-context.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import "@fastify/request-context";
|
||||||
|
|
||||||
|
declare module "@fastify/request-context" {
|
||||||
|
interface RequestContextData {
|
||||||
|
requestId: string;
|
||||||
|
}
|
||||||
|
}
|
4
backend/src/@types/fastify-zod.d.ts
vendored
4
backend/src/@types/fastify-zod.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
|
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
|
||||||
import { Logger } from "pino";
|
|
||||||
|
|
||||||
|
import { CustomLogger } from "@app/lib/logger/logger";
|
||||||
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
|
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -8,7 +8,7 @@ declare global {
|
|||||||
RawServerDefault,
|
RawServerDefault,
|
||||||
RawRequestDefaultExpression<RawServerDefault>,
|
RawRequestDefaultExpression<RawServerDefault>,
|
||||||
RawReplyDefaultExpression<RawServerDefault>,
|
RawReplyDefaultExpression<RawServerDefault>,
|
||||||
Readonly<Logger>,
|
Readonly<CustomLogger>,
|
||||||
ZodTypeProvider
|
ZodTypeProvider
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@ -18,6 +18,7 @@ import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-con
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||||
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
||||||
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
|
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
|
||||||
@ -43,6 +44,7 @@ import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
|||||||
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||||
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
|
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||||
@ -77,6 +79,7 @@ import { TServiceTokenServiceFactory } from "@app/services/service-token/service
|
|||||||
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
||||||
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||||
|
import { TTotpServiceFactory } from "@app/services/totp/totp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
import { TUserServiceFactory } from "@app/services/user/user-service";
|
import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||||
@ -183,12 +186,15 @@ declare module "fastify" {
|
|||||||
rateLimit: TRateLimitServiceFactory;
|
rateLimit: TRateLimitServiceFactory;
|
||||||
userEngagement: TUserEngagementServiceFactory;
|
userEngagement: TUserEngagementServiceFactory;
|
||||||
externalKms: TExternalKmsServiceFactory;
|
externalKms: TExternalKmsServiceFactory;
|
||||||
|
hsm: THsmServiceFactory;
|
||||||
orgAdmin: TOrgAdminServiceFactory;
|
orgAdmin: TOrgAdminServiceFactory;
|
||||||
slack: TSlackServiceFactory;
|
slack: TSlackServiceFactory;
|
||||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||||
cmek: TCmekServiceFactory;
|
cmek: TCmekServiceFactory;
|
||||||
migration: TExternalMigrationServiceFactory;
|
migration: TExternalMigrationServiceFactory;
|
||||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||||
|
projectTemplate: TProjectTemplateServiceFactory;
|
||||||
|
totp: TTotpServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
12
backend/src/@types/knex.d.ts
vendored
12
backend/src/@types/knex.d.ts
vendored
@ -200,6 +200,9 @@ import {
|
|||||||
TProjectSlackConfigsInsert,
|
TProjectSlackConfigsInsert,
|
||||||
TProjectSlackConfigsUpdate,
|
TProjectSlackConfigsUpdate,
|
||||||
TProjectsUpdate,
|
TProjectsUpdate,
|
||||||
|
TProjectTemplates,
|
||||||
|
TProjectTemplatesInsert,
|
||||||
|
TProjectTemplatesUpdate,
|
||||||
TProjectUserAdditionalPrivilege,
|
TProjectUserAdditionalPrivilege,
|
||||||
TProjectUserAdditionalPrivilegeInsert,
|
TProjectUserAdditionalPrivilegeInsert,
|
||||||
TProjectUserAdditionalPrivilegeUpdate,
|
TProjectUserAdditionalPrivilegeUpdate,
|
||||||
@ -311,6 +314,9 @@ import {
|
|||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
|
TTotpConfigs,
|
||||||
|
TTotpConfigsInsert,
|
||||||
|
TTotpConfigsUpdate,
|
||||||
TTrustedIps,
|
TTrustedIps,
|
||||||
TTrustedIpsInsert,
|
TTrustedIpsInsert,
|
||||||
TTrustedIpsUpdate,
|
TTrustedIpsUpdate,
|
||||||
@ -818,5 +824,11 @@ declare module "knex/types/tables" {
|
|||||||
TExternalGroupOrgRoleMappingsInsert,
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
TExternalGroupOrgRoleMappingsUpdate
|
TExternalGroupOrgRoleMappingsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.ProjectTemplates]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectTemplates,
|
||||||
|
TProjectTemplatesInsert,
|
||||||
|
TProjectTemplatesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,23 +64,25 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
const hasCaCertIdColumn = await knex.schema.hasColumn(TableName.Certificate, "caCertId");
|
||||||
t.uuid("caCertId").nullable();
|
if (!hasCaCertIdColumn) {
|
||||||
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
});
|
t.uuid("caCertId").nullable();
|
||||||
|
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
|
||||||
|
});
|
||||||
|
|
||||||
await knex.raw(`
|
await knex.raw(`
|
||||||
UPDATE "${TableName.Certificate}" cert
|
UPDATE "${TableName.Certificate}" cert
|
||||||
SET "caCertId" = (
|
SET "caCertId" = (
|
||||||
SELECT caCert.id
|
SELECT caCert.id
|
||||||
FROM "${TableName.CertificateAuthorityCert}" caCert
|
FROM "${TableName.CertificateAuthorityCert}" caCert
|
||||||
WHERE caCert."caId" = cert."caId"
|
WHERE caCert."caId" = cert."caId"
|
||||||
)
|
)`);
|
||||||
`);
|
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
t.uuid("caCertId").notNullable().alter();
|
t.uuid("caCertId").notNullable().alter();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
const BATCH_SIZE = 30_000;
|
const BATCH_SIZE = 10_000;
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
|
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
|
||||||
@ -12,7 +12,18 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.string("authMethod").nullable();
|
t.string("authMethod").nullable();
|
||||||
});
|
});
|
||||||
|
|
||||||
let nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
|
// first we remove identities without auth method that is unused
|
||||||
|
// ! We delete all access tokens where the identity has no auth method set!
|
||||||
|
// ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted.
|
||||||
|
await knex(TableName.IdentityAccessToken)
|
||||||
|
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
|
||||||
|
.whereNull(`${TableName.Identity}.authMethod`)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
let nullableAccessTokens = await knex(TableName.IdentityAccessToken)
|
||||||
|
.whereNull("authMethod")
|
||||||
|
.limit(BATCH_SIZE)
|
||||||
|
.select("id");
|
||||||
let totalUpdated = 0;
|
let totalUpdated = 0;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@ -33,24 +44,15 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
|
nullableAccessTokens = await knex(TableName.IdentityAccessToken)
|
||||||
|
.whereNull("authMethod")
|
||||||
|
.limit(BATCH_SIZE)
|
||||||
|
.select("id");
|
||||||
|
|
||||||
totalUpdated += batchIds.length;
|
totalUpdated += batchIds.length;
|
||||||
console.log(`Updated ${batchIds.length} access tokens in batch <> Total updated: ${totalUpdated}`);
|
console.log(`Updated ${batchIds.length} access tokens in batch <> Total updated: ${totalUpdated}`);
|
||||||
} while (nullableAccessTokens.length > 0);
|
} while (nullableAccessTokens.length > 0);
|
||||||
|
|
||||||
// ! We delete all access tokens where the identity has no auth method set!
|
|
||||||
// ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted.
|
|
||||||
await knex(TableName.IdentityAccessToken)
|
|
||||||
.whereNotExists((queryBuilder) => {
|
|
||||||
void queryBuilder
|
|
||||||
.select("id")
|
|
||||||
.from(TableName.Identity)
|
|
||||||
.whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`)
|
|
||||||
.whereNotNull("authMethod");
|
|
||||||
})
|
|
||||||
.delete();
|
|
||||||
|
|
||||||
// Finally we set the authMethod to notNullable after populating the column.
|
// Finally we set the authMethod to notNullable after populating the column.
|
||||||
// This will fail if the data is not populated correctly, so it's safe.
|
// This will fail if the data is not populated correctly, so it's safe.
|
||||||
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
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.ProjectTemplates))) {
|
||||||
|
await knex.schema.createTable(TableName.ProjectTemplates, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name", 32).notNullable();
|
||||||
|
t.string("description").nullable();
|
||||||
|
t.jsonb("roles").notNullable();
|
||||||
|
t.jsonb("environments").notNullable();
|
||||||
|
t.uuid("orgId").notNullable().references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ProjectTemplates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.ProjectTemplates)) {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectTemplates);
|
||||||
|
|
||||||
|
await knex.schema.dropTable(TableName.ProjectTemplates);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn(
|
||||||
|
TableName.CertificateTemplateEstConfig,
|
||||||
|
"disableBootstrapCertValidation"
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
|
||||||
|
if (!hasDisableBootstrapCertValidationCol) {
|
||||||
|
t.boolean("disableBootstrapCertValidation").defaultTo(false).notNullable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCaChainCol) {
|
||||||
|
t.binary("encryptedCaChain").nullable().alter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn(
|
||||||
|
TableName.CertificateTemplateEstConfig,
|
||||||
|
"disableBootstrapCertValidation"
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
|
||||||
|
if (hasDisableBootstrapCertValidationCol) {
|
||||||
|
t.dropColumn("disableBootstrapCertValidation");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
23
backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts
Normal file
23
backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
|
||||||
|
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
|
||||||
|
if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("SOFTWARE");
|
||||||
|
if (!hasTimestampsCol) t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
|
||||||
|
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
|
||||||
|
if (hasEncryptionStrategy) t.dropColumn("encryptionStrategy");
|
||||||
|
if (hasTimestampsCol) t.dropTimestamps(true);
|
||||||
|
});
|
||||||
|
}
|
54
backend/src/db/migrations/20241112082701_add-totp-support.ts
Normal file
54
backend/src/db/migrations/20241112082701_add-totp-support.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.TotpConfig))) {
|
||||||
|
await knex.schema.createTable(TableName.TotpConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("userId").notNullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.boolean("isVerified").defaultTo(false).notNullable();
|
||||||
|
t.binary("encryptedRecoveryCodes").notNullable();
|
||||||
|
t.binary("encryptedSecret").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.unique("userId");
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.TotpConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
if (!doesOrgMfaMethodColExist) {
|
||||||
|
t.string("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (!doesUserSelectedMfaMethodColExist) {
|
||||||
|
t.string("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.TotpConfig);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.TotpConfig);
|
||||||
|
|
||||||
|
const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
if (doesOrgMfaMethodColExist) {
|
||||||
|
t.dropColumn("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (doesUserSelectedMfaMethodColExist) {
|
||||||
|
t.dropColumn("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description");
|
||||||
|
|
||||||
|
if (!hasProjectDescription) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.string("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description");
|
||||||
|
|
||||||
|
if (hasProjectDescription) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.dropColumn("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
|
||||||
|
await knex(TableName.IdentityMetadata).whereNull("value").delete();
|
||||||
|
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
|
||||||
|
t.string("value", 1020).notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
|
||||||
|
t.string("value", 1020).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -12,11 +12,12 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const CertificateTemplateEstConfigsSchema = z.object({
|
export const CertificateTemplateEstConfigsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
encryptedCaChain: zodBuffer,
|
encryptedCaChain: zodBuffer.nullable().optional(),
|
||||||
hashedPassphrase: z.string(),
|
hashedPassphrase: z.string(),
|
||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
disableBootstrapCertValidation: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
||||||
|
@ -64,6 +64,7 @@ export * from "./project-keys";
|
|||||||
export * from "./project-memberships";
|
export * from "./project-memberships";
|
||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
export * from "./project-slack-configs";
|
export * from "./project-slack-configs";
|
||||||
|
export * from "./project-templates";
|
||||||
export * from "./project-user-additional-privilege";
|
export * from "./project-user-additional-privilege";
|
||||||
export * from "./project-user-membership-roles";
|
export * from "./project-user-membership-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
@ -105,6 +106,7 @@ export * from "./secrets-v2";
|
|||||||
export * from "./service-tokens";
|
export * from "./service-tokens";
|
||||||
export * from "./slack-integrations";
|
export * from "./slack-integrations";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
|
export * from "./totp-configs";
|
||||||
export * from "./trusted-ips";
|
export * from "./trusted-ips";
|
||||||
export * from "./user-actions";
|
export * from "./user-actions";
|
||||||
export * from "./user-aliases";
|
export * from "./user-aliases";
|
||||||
|
@ -11,7 +11,10 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const KmsRootConfigSchema = z.object({
|
export const KmsRootConfigSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedRootKey: zodBuffer
|
encryptedRootKey: zodBuffer,
|
||||||
|
encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
|
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
|
||||||
|
@ -41,6 +41,7 @@ export enum TableName {
|
|||||||
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
|
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
|
||||||
ProjectUserMembershipRole = "project_user_membership_roles",
|
ProjectUserMembershipRole = "project_user_membership_roles",
|
||||||
ProjectKeys = "project_keys",
|
ProjectKeys = "project_keys",
|
||||||
|
ProjectTemplates = "project_templates",
|
||||||
Secret = "secrets",
|
Secret = "secrets",
|
||||||
SecretReference = "secret_references",
|
SecretReference = "secret_references",
|
||||||
SecretSharing = "secret_sharing",
|
SecretSharing = "secret_sharing",
|
||||||
@ -116,6 +117,7 @@ export enum TableName {
|
|||||||
ExternalKms = "external_kms",
|
ExternalKms = "external_kms",
|
||||||
InternalKms = "internal_kms",
|
InternalKms = "internal_kms",
|
||||||
InternalKmsKeyVersion = "internal_kms_key_version",
|
InternalKmsKeyVersion = "internal_kms_key_version",
|
||||||
|
TotpConfig = "totp_configs",
|
||||||
// @depreciated
|
// @depreciated
|
||||||
KmsKeyVersion = "kms_key_versions",
|
KmsKeyVersion = "kms_key_versions",
|
||||||
WorkflowIntegrations = "workflow_integrations",
|
WorkflowIntegrations = "workflow_integrations",
|
||||||
|
@ -21,7 +21,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
||||||
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false)
|
enforceMfa: z.boolean().default(false),
|
||||||
|
selectedMfaMethod: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
@ -15,7 +15,8 @@ export const ProjectRolesSchema = z.object({
|
|||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
projectId: z.string()
|
projectId: z.string(),
|
||||||
|
version: z.number().default(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjectRoles = z.infer<typeof ProjectRolesSchema>;
|
export type TProjectRoles = z.infer<typeof ProjectRolesSchema>;
|
||||||
|
23
backend/src/db/schemas/project-templates.ts
Normal file
23
backend/src/db/schemas/project-templates.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 ProjectTemplatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
roles: z.unknown(),
|
||||||
|
environments: z.unknown(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>;
|
||||||
|
export type TProjectTemplatesInsert = Omit<z.input<typeof ProjectTemplatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TProjectTemplatesUpdate = Partial<Omit<z.input<typeof ProjectTemplatesSchema>, TImmutableDBKeys>>;
|
@ -23,7 +23,8 @@ export const ProjectsSchema = z.object({
|
|||||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||||
auditLogsRetentionDays: z.number().nullable().optional(),
|
auditLogsRetentionDays: z.number().nullable().optional(),
|
||||||
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
|
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
|
||||||
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
|
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||||
|
description: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
24
backend/src/db/schemas/totp-configs.ts
Normal file
24
backend/src/db/schemas/totp-configs.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const TotpConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
isVerified: z.boolean().default(false),
|
||||||
|
encryptedRecoveryCodes: zodBuffer,
|
||||||
|
encryptedSecret: zodBuffer,
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TTotpConfigs = z.infer<typeof TotpConfigsSchema>;
|
||||||
|
export type TTotpConfigsInsert = Omit<z.input<typeof TotpConfigsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TTotpConfigsUpdate = Partial<Omit<z.input<typeof TotpConfigsSchema>, TImmutableDBKeys>>;
|
@ -26,7 +26,8 @@ export const UsersSchema = z.object({
|
|||||||
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
||||||
isLocked: z.boolean().default(false).nullable().optional(),
|
isLocked: z.boolean().default(false).nullable().optional(),
|
||||||
temporaryLockDateEnd: z.date().nullable().optional(),
|
temporaryLockDateEnd: z.date().nullable().optional(),
|
||||||
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
|
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional(),
|
||||||
|
selectedMfaMethod: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
@ -2,6 +2,9 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { TableName } from "./schemas";
|
import { TableName } from "./schemas";
|
||||||
|
|
||||||
|
interface PgTriggerResult {
|
||||||
|
rows: Array<{ exists: boolean }>;
|
||||||
|
}
|
||||||
export const createJunctionTable = (knex: Knex, tableName: TableName, table1Name: TableName, table2Name: TableName) =>
|
export const createJunctionTable = (knex: Knex, tableName: TableName, table1Name: TableName, table2Name: TableName) =>
|
||||||
knex.schema.createTable(tableName, (table) => {
|
knex.schema.createTable(tableName, (table) => {
|
||||||
table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
@ -28,13 +31,26 @@ DROP FUNCTION IF EXISTS on_update_timestamp() CASCADE;
|
|||||||
|
|
||||||
// we would be using this to apply updatedAt where ever we wanta
|
// we would be using this to apply updatedAt where ever we wanta
|
||||||
// remember to set `timestamps(true,true,true)` before this on schema
|
// remember to set `timestamps(true,true,true)` before this on schema
|
||||||
export const createOnUpdateTrigger = (knex: Knex, tableName: string) =>
|
export const createOnUpdateTrigger = async (knex: Knex, tableName: string) => {
|
||||||
knex.raw(`
|
const triggerExists = await knex.raw<PgTriggerResult>(`
|
||||||
CREATE TRIGGER "${tableName}_updatedAt"
|
SELECT EXISTS (
|
||||||
BEFORE UPDATE ON ${tableName}
|
SELECT 1
|
||||||
FOR EACH ROW
|
FROM pg_trigger
|
||||||
EXECUTE PROCEDURE on_update_timestamp();
|
WHERE tgname = '${tableName}_updatedAt'
|
||||||
`);
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!triggerExists?.rows?.[0]?.exists) {
|
||||||
|
return knex.raw(`
|
||||||
|
CREATE TRIGGER "${tableName}_updatedAt"
|
||||||
|
BEFORE UPDATE ON ${tableName}
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE on_update_timestamp();
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const dropOnUpdateTrigger = (knex: Knex, tableName: string) =>
|
export const dropOnUpdateTrigger = (knex: Knex, tableName: string) =>
|
||||||
knex.raw(`DROP TRIGGER IF EXISTS "${tableName}_updatedAt" ON ${tableName}`);
|
knex.raw(`DROP TRIGGER IF EXISTS "${tableName}_updatedAt" ON ${tableName}`);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -8,6 +7,7 @@ import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
|||||||
import { daysToMillisecond } from "@app/lib/dates";
|
import { daysToMillisecond } from "@app/lib/dates";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -48,15 +48,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
||||||
name: z
|
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
|
||||||
.string()
|
|
||||||
.describe(DYNAMIC_SECRETS.CREATE.name)
|
|
||||||
.min(1)
|
|
||||||
.toLowerCase()
|
|
||||||
.max(64)
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid"
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
|
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||||
import { GROUPS } from "@app/lib/api-docs";
|
import { GROUPS } from "@app/lib/api-docs";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@ -14,15 +14,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
||||||
slug: z
|
slug: slugSchema({ min: 5, max: 36 }).optional().describe(GROUPS.CREATE.slug),
|
||||||
.string()
|
|
||||||
.min(5)
|
|
||||||
.max(36)
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.describe(GROUPS.CREATE.slug),
|
|
||||||
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
|
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -100,14 +92,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
|
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
|
||||||
slug: z
|
slug: slugSchema({ min: 5, max: 36 }).describe(GROUPS.UPDATE.slug),
|
||||||
.string()
|
|
||||||
.min(5)
|
|
||||||
.max(36)
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.describe(GROUPS.UPDATE.slug),
|
|
||||||
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
|
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial(),
|
||||||
|
@ -8,6 +8,7 @@ import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
|||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionSchema,
|
ProjectPermissionSchema,
|
||||||
@ -33,17 +34,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
|
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
|
||||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
|
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(60)
|
|
||||||
.trim()
|
|
||||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
|
||||||
permissions: ProjectPermissionSchema.array()
|
permissions: ProjectPermissionSchema.array()
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||||
.optional(),
|
.optional(),
|
||||||
@ -77,7 +68,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
...req.body,
|
...req.body,
|
||||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
|
||||||
isTemporary: false,
|
isTemporary: false,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore-error this is valid ts
|
// @ts-ignore-error this is valid ts
|
||||||
@ -103,17 +94,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
|
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
|
||||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
|
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(60)
|
|
||||||
.trim()
|
|
||||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
|
||||||
permissions: ProjectPermissionSchema.array()
|
permissions: ProjectPermissionSchema.array()
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||||
.optional(),
|
.optional(),
|
||||||
@ -159,7 +140,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
...req.body,
|
...req.body,
|
||||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore-error this is valid ts
|
// @ts-ignore-error this is valid ts
|
||||||
@ -189,16 +170,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
|
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
|
||||||
privilegeDetails: z
|
privilegeDetails: z
|
||||||
.object({
|
.object({
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(60)
|
|
||||||
.trim()
|
|
||||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
|
||||||
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||||
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
|
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
|
||||||
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission
|
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-template-router";
|
||||||
|
|
||||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||||
@ -92,4 +94,6 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerExternalKmsRouter, {
|
await server.register(registerExternalKmsRouter, {
|
||||||
prefix: "/external-kms"
|
prefix: "/external-kms"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@ -18,17 +18,10 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
organizationId: z.string().trim()
|
organizationId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 64 }).refine(
|
||||||
.string()
|
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
|
||||||
.min(1)
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
.trim()
|
),
|
||||||
.refine(
|
|
||||||
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
|
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
|
||||||
)
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid"
|
|
||||||
}),
|
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
permissions: z.any().array()
|
permissions: z.any().array()
|
||||||
@ -94,17 +87,13 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
roleId: z.string().trim()
|
roleId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z
|
// TODO: Switch to slugSchema after verifying correct methods with Akhil - Omar 11/24
|
||||||
.string()
|
slug: slugSchema({ min: 1, max: 64 })
|
||||||
.trim()
|
|
||||||
.optional()
|
|
||||||
.refine(
|
.refine(
|
||||||
(val) => typeof val !== "undefined" && !Object.keys(OrgMembershipRole).includes(val),
|
(val) => !Object.keys(OrgMembershipRole).includes(val),
|
||||||
"Please choose a different slug, the slug you have entered is reserved."
|
"Please choose a different slug, the slug you have entered is reserved."
|
||||||
)
|
)
|
||||||
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
.optional(),
|
||||||
message: "Slug must be a valid"
|
|
||||||
}),
|
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
permissions: z.any().array().optional()
|
permissions: z.any().array().optional()
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { packRules } from "@casl/ability/extra";
|
import { packRules } from "@casl/ability/extra";
|
||||||
import slugify from "@sindresorhus/slugify";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
@ -9,6 +8,7 @@ import {
|
|||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -32,18 +32,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z
|
slug: slugSchema({ max: 64 })
|
||||||
.string()
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.refine(
|
.refine(
|
||||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
)
|
)
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid"
|
|
||||||
})
|
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
||||||
@ -94,21 +87,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z
|
slug: slugSchema()
|
||||||
.string()
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.optional()
|
|
||||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
|
||||||
.refine(
|
.refine(
|
||||||
(val) =>
|
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||||
typeof val === "undefined" ||
|
|
||||||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
)
|
)
|
||||||
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||||
message: "Slug must be a valid"
|
.optional(),
|
||||||
}),
|
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
@ -192,7 +177,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
roles: ProjectRolesSchema.omit({ permissions: true }).array()
|
roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -225,7 +210,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
role: SanitizedRoleSchemaV1
|
role: SanitizedRoleSchemaV1.omit({ version: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
302
backend/src/ee/routes/v1/project-template-router.ts
Normal file
302
backend/src/ee/routes/v1/project-template-router.ts
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
||||||
|
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||||
|
import { ProjectTemplates } from "@app/lib/api-docs";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768;
|
||||||
|
|
||||||
|
const isReservedRoleSlug = (slug: string) =>
|
||||||
|
Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
||||||
|
|
||||||
|
const isReservedRoleName = (name: string) =>
|
||||||
|
["custom", "admin", "viewer", "developer", "no access"].includes(name.toLowerCase());
|
||||||
|
|
||||||
|
const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
|
||||||
|
roles: z
|
||||||
|
.object({
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
slug: slugSchema(),
|
||||||
|
permissions: UnpackedPermissionSchema.array()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
environments: z
|
||||||
|
.object({
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
slug: slugSchema(),
|
||||||
|
position: z.number().min(1)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProjectTemplateRolesSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
slug: slugSchema(),
|
||||||
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.superRefine((roles, ctx) => {
|
||||||
|
if (!roles.length) return;
|
||||||
|
|
||||||
|
if (Buffer.byteLength(JSON.stringify(roles)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
|
||||||
|
|
||||||
|
if (new Set(roles.map((v) => v.slug)).size !== roles.length)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" });
|
||||||
|
|
||||||
|
if (new Set(roles.map((v) => v.name)).size !== roles.length)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role names must be unique" });
|
||||||
|
|
||||||
|
roles.forEach((role) => {
|
||||||
|
if (isReservedRoleSlug(role.slug))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Role slug "${role.slug}" is reserved` });
|
||||||
|
|
||||||
|
if (isReservedRoleName(role.name))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Role name "${role.name}" is reserved` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProjectTemplateEnvironmentsSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
slug: slugSchema(),
|
||||||
|
position: z.number().min(1)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.superRefine((environments, ctx) => {
|
||||||
|
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
|
||||||
|
|
||||||
|
if (new Set(environments.map((v) => v.name)).size !== environments.length)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment names must be unique" });
|
||||||
|
|
||||||
|
if (new Set(environments.map((v) => v.slug)).size !== environments.length)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment slugs must be unique" });
|
||||||
|
|
||||||
|
if (
|
||||||
|
environments.some((env) => env.position < 1 || env.position > environments.length) ||
|
||||||
|
new Set(environments.map((env) => env.position)).size !== environments.length
|
||||||
|
)
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "One or more of the positions specified is invalid. Positions must be sequential starting from 1."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerProjectTemplateRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List project templates for the current organization.",
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projectTemplates: SanitizedProjectTemplateSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
|
||||||
|
|
||||||
|
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PROJECT_TEMPLATES,
|
||||||
|
metadata: {
|
||||||
|
count: auditTemplates.length,
|
||||||
|
templateIds: auditTemplates.map((template) => template.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { projectTemplates };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:templateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get a project template by ID.",
|
||||||
|
params: z.object({
|
||||||
|
templateId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projectTemplate: SanitizedProjectTemplateSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const projectTemplate = await server.services.projectTemplate.findProjectTemplateById(
|
||||||
|
req.params.templateId,
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PROJECT_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
templateId: req.params.templateId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { projectTemplate };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Create a project template.",
|
||||||
|
body: z.object({
|
||||||
|
name: slugSchema({ field: "name" })
|
||||||
|
.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||||
|
message: `The requested project template name is reserved.`
|
||||||
|
})
|
||||||
|
.describe(ProjectTemplates.CREATE.name),
|
||||||
|
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
||||||
|
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
||||||
|
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
|
||||||
|
ProjectTemplates.CREATE.environments
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projectTemplate: SanitizedProjectTemplateSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const projectTemplate = await server.services.projectTemplate.createProjectTemplate(req.body, req.permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_PROJECT_TEMPLATE,
|
||||||
|
metadata: req.body
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { projectTemplate };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:templateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Update a project template.",
|
||||||
|
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
|
||||||
|
body: z.object({
|
||||||
|
name: slugSchema({ field: "name" })
|
||||||
|
.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||||
|
message: `The requested project template name is reserved.`
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(ProjectTemplates.UPDATE.name),
|
||||||
|
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
|
||||||
|
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
|
||||||
|
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projectTemplate: SanitizedProjectTemplateSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const projectTemplate = await server.services.projectTemplate.updateProjectTemplateById(
|
||||||
|
req.params.templateId,
|
||||||
|
req.body,
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_PROJECT_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
templateId: req.params.templateId,
|
||||||
|
...req.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { projectTemplate };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:templateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Delete a project template.",
|
||||||
|
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.DELETE.templateId) }),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projectTemplate: SanitizedProjectTemplateSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const projectTemplate = await server.services.projectTemplate.deleteProjectTemplateById(
|
||||||
|
req.params.templateId,
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_PROJECT_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
templateId: req.params.templateId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { projectTemplate };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -122,6 +122,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
`email: ${email} firstName: ${profile.firstName as string}`
|
`email: ${email} firstName: ${profile.firstName as string}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
throw new Error("Invalid saml request. Missing email or first name");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMetadata = Object.keys(profile.attributes || {})
|
const userMetadata = Object.keys(profile.attributes || {})
|
||||||
|
@ -7,6 +7,7 @@ import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/pr
|
|||||||
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege";
|
import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -21,17 +22,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(60)
|
|
||||||
.trim()
|
|
||||||
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({
|
z.object({
|
||||||
@ -87,15 +78,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
}),
|
}),
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||||
.string()
|
|
||||||
.max(60)
|
|
||||||
.trim()
|
|
||||||
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
|
||||||
permissions: ProjectPermissionV2Schema.array()
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||||
|
@ -7,6 +7,7 @@ import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-p
|
|||||||
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
|
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege";
|
import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -28,17 +29,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
|
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
|
||||||
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
|
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(60)
|
|
||||||
.trim()
|
|
||||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
|
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({
|
z.object({
|
||||||
@ -100,16 +91,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id)
|
id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(60)
|
|
||||||
.trim()
|
|
||||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid slug"
|
|
||||||
})
|
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
|
||||||
permissions: ProjectPermissionV2Schema.array()
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { packRules } from "@casl/ability/extra";
|
import { packRules } from "@casl/ability/extra";
|
||||||
import slugify from "@sindresorhus/slugify";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -29,18 +29,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
|
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 64 })
|
||||||
.string()
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.refine(
|
.refine(
|
||||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
)
|
)
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid"
|
|
||||||
})
|
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
||||||
@ -90,21 +83,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 64 })
|
||||||
.string()
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.optional()
|
|
||||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
|
||||||
.refine(
|
.refine(
|
||||||
(val) =>
|
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||||
typeof val === "undefined" ||
|
|
||||||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
)
|
)
|
||||||
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
.optional()
|
||||||
message: "Slug must be a valid"
|
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||||
}),
|
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
@ -186,7 +171,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
roles: ProjectRolesSchema.omit({ permissions: true }).array()
|
roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -219,7 +204,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
role: SanitizedRoleSchema
|
role: SanitizedRoleSchema.omit({ version: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
TCreateProjectTemplateDTO,
|
||||||
|
TUpdateProjectTemplateDTO
|
||||||
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@ -192,7 +196,13 @@ export enum EventType {
|
|||||||
CMEK_ENCRYPT = "cmek-encrypt",
|
CMEK_ENCRYPT = "cmek-encrypt",
|
||||||
CMEK_DECRYPT = "cmek-decrypt",
|
CMEK_DECRYPT = "cmek-decrypt",
|
||||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping"
|
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
||||||
|
GET_PROJECT_TEMPLATES = "get-project-templates",
|
||||||
|
GET_PROJECT_TEMPLATE = "get-project-template",
|
||||||
|
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
||||||
|
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
||||||
|
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
||||||
|
APPLY_PROJECT_TEMPLATE = "apply-project-template"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@ -1618,6 +1628,46 @@ interface UpdateExternalGroupOrgRoleMappingsEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetProjectTemplatesEvent {
|
||||||
|
type: EventType.GET_PROJECT_TEMPLATES;
|
||||||
|
metadata: {
|
||||||
|
count: number;
|
||||||
|
templateIds: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetProjectTemplateEvent {
|
||||||
|
type: EventType.GET_PROJECT_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
templateId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateProjectTemplateEvent {
|
||||||
|
type: EventType.CREATE_PROJECT_TEMPLATE;
|
||||||
|
metadata: TCreateProjectTemplateDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateProjectTemplateEvent {
|
||||||
|
type: EventType.UPDATE_PROJECT_TEMPLATE;
|
||||||
|
metadata: TUpdateProjectTemplateDTO & { templateId: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteProjectTemplateEvent {
|
||||||
|
type: EventType.DELETE_PROJECT_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
templateId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApplyProjectTemplateEvent {
|
||||||
|
type: EventType.APPLY_PROJECT_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
template: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@ -1766,4 +1816,10 @@ export type Event =
|
|||||||
| CmekEncryptEvent
|
| CmekEncryptEvent
|
||||||
| CmekDecryptEvent
|
| CmekDecryptEvent
|
||||||
| GetExternalGroupOrgRoleMappingsEvent
|
| GetExternalGroupOrgRoleMappingsEvent
|
||||||
| UpdateExternalGroupOrgRoleMappingsEvent;
|
| UpdateExternalGroupOrgRoleMappingsEvent
|
||||||
|
| GetProjectTemplatesEvent
|
||||||
|
| GetProjectTemplateEvent
|
||||||
|
| CreateProjectTemplateEvent
|
||||||
|
| UpdateProjectTemplateEvent
|
||||||
|
| DeleteProjectTemplateEvent
|
||||||
|
| ApplyProjectTemplateEvent;
|
||||||
|
@ -171,27 +171,29 @@ export const certificateEstServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const caCerts = estConfig.caChain
|
if (!estConfig.disableBootstrapCertValidation) {
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
const caCerts = estConfig.caChain
|
||||||
?.map((cert) => {
|
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||||
return new x509.X509Certificate(cert);
|
?.map((cert) => {
|
||||||
});
|
return new x509.X509Certificate(cert);
|
||||||
|
});
|
||||||
|
|
||||||
if (!caCerts) {
|
if (!caCerts) {
|
||||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||||
)?.[0];
|
)?.[0];
|
||||||
|
|
||||||
if (!leafCertificate) {
|
if (!leafCertificate) {
|
||||||
throw new BadRequestError({ message: "Missing client certificate" });
|
throw new BadRequestError({ message: "Missing client certificate" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const certObj = new x509.X509Certificate(leafCertificate);
|
const certObj = new x509.X509Certificate(leafCertificate);
|
||||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrderByDirection, ProjectServiceActor } from "@app/lib/types";
|
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
@ -457,7 +457,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
|
|
||||||
const listDynamicSecretsByFolderIds = async (
|
const listDynamicSecretsByFolderIds = async (
|
||||||
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
|
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
|
||||||
actor: ProjectServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor.type,
|
actor.type,
|
||||||
|
@ -80,7 +80,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUserToInfisicalGroup = async (userId: string) => {
|
const $addUserToInfisicalGroup = async (userId: string) => {
|
||||||
// figure out if the default user is already in the group, if it is, then we shouldn't add it again
|
// figure out if the default user is already in the group, if it is, then we shouldn't add it again
|
||||||
|
|
||||||
const addUserToGroupCommand = new ModifyUserGroupCommand({
|
const addUserToGroupCommand = new ModifyUserGroupCommand({
|
||||||
@ -96,7 +96,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
|
|||||||
await ensureInfisicalGroupExists(clusterName);
|
await ensureInfisicalGroupExists(clusterName);
|
||||||
|
|
||||||
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
|
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
|
||||||
await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
|
await $addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: creationInput.UserId,
|
userId: creationInput.UserId,
|
||||||
@ -212,7 +212,7 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
// Do nothing
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
||||||
const client = new IAMClient({
|
const client = new IAMClient({
|
||||||
region: providerInputs.region,
|
region: providerInputs.region,
|
||||||
credentials: {
|
credentials: {
|
||||||
@ -47,7 +47,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
||||||
return isConnected;
|
return isConnected;
|
||||||
@ -55,7 +55,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||||
@ -118,7 +118,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
|
|
||||||
@ -179,9 +179,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (_inputs: unknown, entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
// do nothing
|
// No renewal necessary
|
||||||
const username = entityId;
|
return { entityId };
|
||||||
return { entityId: username };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -23,7 +23,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getToken = async (
|
const $getToken = async (
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
applicationId: string,
|
applicationId: string,
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
@ -51,18 +51,13 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||||
return data.success;
|
return data.success;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
|
||||||
// Do nothing
|
|
||||||
return { entityId };
|
|
||||||
};
|
|
||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||||
}
|
}
|
||||||
@ -98,7 +93,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
||||||
const data = await getToken(tenantId, applicationId, clientSecret);
|
const data = await $getToken(tenantId, applicationId, clientSecret);
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||||
}
|
}
|
||||||
@ -127,6 +122,11 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
|||||||
return users;
|
return users;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// No renewal necessary
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validateProviderInputs,
|
validateProviderInputs,
|
||||||
validateConnection,
|
validateConnection,
|
||||||
|
@ -27,7 +27,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||||
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||||
const client = new cassandra.Client({
|
const client = new cassandra.Client({
|
||||||
sslOptions,
|
sslOptions,
|
||||||
@ -47,7 +47,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||||
await client.shutdown();
|
await client.shutdown();
|
||||||
@ -56,7 +56,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@ -82,7 +82,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const { keyspace } = providerInputs;
|
const { keyspace } = providerInputs;
|
||||||
@ -99,20 +99,24 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
const { keyspace } = providerInputs;
|
const { keyspace } = providerInputs;
|
||||||
|
|
||||||
const renewStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace, expiration });
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||||
|
username: entityId,
|
||||||
|
keyspace,
|
||||||
|
expiration
|
||||||
|
});
|
||||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
for (const query of queries) {
|
for await (const query of queries) {
|
||||||
// eslint-disable-next-line
|
|
||||||
await client.execute(query);
|
await client.execute(query);
|
||||||
}
|
}
|
||||||
await client.shutdown();
|
await client.shutdown();
|
||||||
return { entityId: username };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -24,7 +24,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||||
const connection = new ElasticSearchClient({
|
const connection = new ElasticSearchClient({
|
||||||
node: {
|
node: {
|
||||||
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
||||||
@ -55,7 +55,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const infoResponse = await connection
|
const infoResponse = await connection
|
||||||
.info()
|
.info()
|
||||||
@ -67,7 +67,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@ -85,7 +85,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
await connection.security.deleteUser({
|
await connection.security.deleteUser({
|
||||||
username: entityId
|
username: entityId
|
||||||
@ -96,7 +96,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
// Do nothing
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,15 +6,17 @@ import { AzureEntraIDProvider } from "./azure-entra-id";
|
|||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { ElasticSearchProvider } from "./elastic-search";
|
import { ElasticSearchProvider } from "./elastic-search";
|
||||||
import { LdapProvider } from "./ldap";
|
import { LdapProvider } from "./ldap";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
|
||||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||||
import { MongoDBProvider } from "./mongo-db";
|
import { MongoDBProvider } from "./mongo-db";
|
||||||
import { RabbitMqProvider } from "./rabbit-mq";
|
import { RabbitMqProvider } from "./rabbit-mq";
|
||||||
import { RedisDatabaseProvider } from "./redis";
|
import { RedisDatabaseProvider } from "./redis";
|
||||||
|
import { SapAseProvider } from "./sap-ase";
|
||||||
import { SapHanaProvider } from "./sap-hana";
|
import { SapHanaProvider } from "./sap-hana";
|
||||||
import { SqlDatabaseProvider } from "./sql-database";
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
|
import { TotpProvider } from "./totp";
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = () => ({
|
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||||
@ -27,5 +29,7 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||||
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
||||||
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
||||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
|
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||||
|
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||||
|
[DynamicSecretProviders.SapAse]: SapAseProvider()
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ import { z } from "zod";
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
import { LdapSchema, TDynamicProviderFns } from "./models";
|
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generatePassword = () => {
|
const generatePassword = () => {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
@ -52,7 +52,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
|
const $getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const client = ldapjs.createClient({
|
const client = ldapjs.createClient({
|
||||||
url: providerInputs.url,
|
url: providerInputs.url,
|
||||||
@ -83,7 +83,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
return client.connected;
|
return client.connected;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -191,37 +191,84 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
||||||
const password = generatePassword();
|
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
|
||||||
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
|
|
||||||
|
|
||||||
try {
|
if (dnMatch) {
|
||||||
const dnArray = await executeLdif(client, generatedLdif);
|
const username = dnMatch[1];
|
||||||
|
const password = generatePassword();
|
||||||
|
|
||||||
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
|
||||||
} catch (err) {
|
|
||||||
if (providerInputs.rollbackLdif) {
|
try {
|
||||||
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
|
const dnArray = await executeLdif(client, generatedLdif);
|
||||||
await executeLdif(client, rollbackLdif);
|
|
||||||
|
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestError({ message: (err as Error).message });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid rotation LDIF, missing DN."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dnArray = await executeLdif(client, generatedLdif);
|
||||||
|
|
||||||
|
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||||
|
} catch (err) {
|
||||||
|
if (providerInputs.rollbackLdif) {
|
||||||
|
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
|
||||||
|
await executeLdif(client, rollbackLdif);
|
||||||
|
}
|
||||||
|
throw new BadRequestError({ message: (err as Error).message });
|
||||||
}
|
}
|
||||||
throw new BadRequestError({ message: (err as Error).message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
|
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
||||||
|
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
|
||||||
|
|
||||||
|
if (dnMatch) {
|
||||||
|
const username = dnMatch[1];
|
||||||
|
const password = generatePassword();
|
||||||
|
|
||||||
|
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dnArray = await executeLdif(client, generatedLdif);
|
||||||
|
|
||||||
|
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestError({ message: (err as Error).message });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid rotation LDIF, missing DN."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
|
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
|
||||||
|
|
||||||
await executeLdif(connection, revocationLdif);
|
await executeLdif(client, revocationLdif);
|
||||||
|
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
// Do nothing
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ export enum SqlProviders {
|
|||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
MySQL = "mysql2",
|
MySQL = "mysql2",
|
||||||
Oracle = "oracledb",
|
Oracle = "oracledb",
|
||||||
MsSQL = "mssql"
|
MsSQL = "mssql",
|
||||||
|
SapAse = "sap-ase"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ElasticSearchAuthTypes {
|
export enum ElasticSearchAuthTypes {
|
||||||
@ -12,6 +13,22 @@ export enum ElasticSearchAuthTypes {
|
|||||||
ApiKey = "api-key"
|
ApiKey = "api-key"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LdapCredentialType {
|
||||||
|
Dynamic = "dynamic",
|
||||||
|
Static = "static"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TotpConfigType {
|
||||||
|
URL = "url",
|
||||||
|
MANUAL = "manual"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TotpAlgorithm {
|
||||||
|
SHA1 = "sha1",
|
||||||
|
SHA256 = "sha256",
|
||||||
|
SHA512 = "sha512"
|
||||||
|
}
|
||||||
|
|
||||||
export const DynamicSecretRedisDBSchema = z.object({
|
export const DynamicSecretRedisDBSchema = z.object({
|
||||||
host: z.string().trim().toLowerCase(),
|
host: z.string().trim().toLowerCase(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
@ -102,6 +119,16 @@ export const DynamicSecretCassandraSchema = z.object({
|
|||||||
ca: z.string().optional()
|
ca: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const DynamicSecretSapAseSchema = z.object({
|
||||||
|
host: z.string().trim().toLowerCase(),
|
||||||
|
port: z.number(),
|
||||||
|
database: z.string().trim(),
|
||||||
|
username: z.string().trim(),
|
||||||
|
password: z.string().trim(),
|
||||||
|
creationStatement: z.string().trim(),
|
||||||
|
revocationStatement: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
export const DynamicSecretAwsIamSchema = z.object({
|
export const DynamicSecretAwsIamSchema = z.object({
|
||||||
accessKey: z.string().trim().min(1),
|
accessKey: z.string().trim().min(1),
|
||||||
secretAccessKey: z.string().trim().min(1),
|
secretAccessKey: z.string().trim().min(1),
|
||||||
@ -195,16 +222,54 @@ export const AzureEntraIDSchema = z.object({
|
|||||||
clientSecret: z.string().trim().min(1)
|
clientSecret: z.string().trim().min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LdapSchema = z.object({
|
export const LdapSchema = z.union([
|
||||||
url: z.string().trim().min(1),
|
z.object({
|
||||||
binddn: z.string().trim().min(1),
|
url: z.string().trim().min(1),
|
||||||
bindpass: z.string().trim().min(1),
|
binddn: z.string().trim().min(1),
|
||||||
ca: z.string().optional(),
|
bindpass: z.string().trim().min(1),
|
||||||
|
ca: z.string().optional(),
|
||||||
|
credentialType: z.literal(LdapCredentialType.Dynamic).optional().default(LdapCredentialType.Dynamic),
|
||||||
|
creationLdif: z.string().min(1),
|
||||||
|
revocationLdif: z.string().min(1),
|
||||||
|
rollbackLdif: z.string().optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
url: z.string().trim().min(1),
|
||||||
|
binddn: z.string().trim().min(1),
|
||||||
|
bindpass: z.string().trim().min(1),
|
||||||
|
ca: z.string().optional(),
|
||||||
|
credentialType: z.literal(LdapCredentialType.Static),
|
||||||
|
rotationLdif: z.string().min(1)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
creationLdif: z.string().min(1),
|
export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
|
||||||
revocationLdif: z.string().min(1),
|
z.object({
|
||||||
rollbackLdif: z.string().optional()
|
configType: z.literal(TotpConfigType.URL),
|
||||||
});
|
url: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine((val) => {
|
||||||
|
const urlObj = new URL(val);
|
||||||
|
const secret = urlObj.searchParams.get("secret");
|
||||||
|
|
||||||
|
return Boolean(secret);
|
||||||
|
}, "OTP URL must contain secret field")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
configType: z.literal(TotpConfigType.MANUAL),
|
||||||
|
secret: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.transform((val) => val.replace(/\s+/g, "")),
|
||||||
|
period: z.number().optional(),
|
||||||
|
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
|
||||||
|
digits: z.number().optional()
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
@ -219,12 +284,15 @@ export enum DynamicSecretProviders {
|
|||||||
AzureEntraID = "azure-entra-id",
|
AzureEntraID = "azure-entra-id",
|
||||||
Ldap = "ldap",
|
Ldap = "ldap",
|
||||||
SapHana = "sap-hana",
|
SapHana = "sap-hana",
|
||||||
Snowflake = "snowflake"
|
Snowflake = "snowflake",
|
||||||
|
Totp = "totp",
|
||||||
|
SapAse = "sap-ase"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.SapAse), inputs: DynamicSecretSapAseSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
|
||||||
@ -235,7 +303,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
|||||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
@ -22,7 +22,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
|
||||||
const client = axios.create({
|
const client = axios.create({
|
||||||
baseURL: "https://cloud.mongodb.com/api/atlas",
|
baseURL: "https://cloud.mongodb.com/api/atlas",
|
||||||
headers: {
|
headers: {
|
||||||
@ -40,7 +40,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const isConnected = await client({
|
const isConnected = await client({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -59,7 +59,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@ -87,7 +87,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const isExisting = await client({
|
const isExisting = await client({
|
||||||
@ -114,7 +114,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
@ -23,7 +23,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
||||||
const isSrv = !providerInputs.port;
|
const isSrv = !providerInputs.port;
|
||||||
const uri = isSrv
|
const uri = isSrv
|
||||||
? `mongodb+srv://${providerInputs.host}`
|
? `mongodb+srv://${providerInputs.host}`
|
||||||
@ -42,7 +42,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const isConnected = await client
|
const isConnected = await client
|
||||||
.db(providerInputs.database)
|
.db(providerInputs.database)
|
||||||
@ -55,7 +55,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@ -74,7 +74,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
|
|
||||||
@ -88,6 +88,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (_inputs: unknown, entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
|
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
|
||||||
auth: {
|
auth: {
|
||||||
@ -105,7 +105,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const infoResponse = await connection.get("/whoami").then(() => true);
|
const infoResponse = await connection.get("/whoami").then(() => true);
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@ -134,7 +134,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
// Do nothing
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
||||||
let connection: Redis | null = null;
|
let connection: Redis | null = null;
|
||||||
try {
|
try {
|
||||||
connection = new Redis({
|
connection = new Redis({
|
||||||
@ -92,7 +92,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const pingResponse = await connection
|
const pingResponse = await connection
|
||||||
.ping()
|
.ping()
|
||||||
@ -104,7 +104,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@ -126,7 +126,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
|
|
||||||
@ -141,7 +141,9 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
145
backend/src/ee/services/dynamic-secret/providers/sap-ase.ts
Normal file
145
backend/src/ee/services/dynamic-secret/providers/sap-ase.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import handlebars from "handlebars";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import odbc from "odbc";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
|
import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const generatePassword = (size = 48) => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
return customAlphabet(charset, 48)(size);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return alphaNumericNanoId(25);
|
||||||
|
};
|
||||||
|
|
||||||
|
enum SapCommands {
|
||||||
|
CreateLogin = "sp_addlogin",
|
||||||
|
DropLogin = "sp_droplogin"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SapAseProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
|
||||||
|
|
||||||
|
verifyHostInputValidity(providerInputs.host);
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
|
||||||
|
const connectionString =
|
||||||
|
`DRIVER={FreeTDS};` +
|
||||||
|
`SERVER=${providerInputs.host};` +
|
||||||
|
`PORT=${providerInputs.port};` +
|
||||||
|
`DATABASE=${useMaster ? "master" : providerInputs.database};` +
|
||||||
|
`UID=${providerInputs.username};` +
|
||||||
|
`PWD=${providerInputs.password};` +
|
||||||
|
`TDS_VERSION=5.0`;
|
||||||
|
|
||||||
|
const client = await odbc.connect(connectionString);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const masterClient = await $getClient(providerInputs, true);
|
||||||
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
|
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||||
|
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||||
|
|
||||||
|
if (!resultFromSelectedDatabase.version) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SAP ASE connection, version query failed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SAP ASE connection (master), version mismatch"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const username = `inf_${generateUsername()}`;
|
||||||
|
const password = `${generatePassword()}`;
|
||||||
|
|
||||||
|
const client = await $getClient(providerInputs);
|
||||||
|
const masterClient = await $getClient(providerInputs, true);
|
||||||
|
|
||||||
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
const queries = creationStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||||
|
|
||||||
|
for await (const query of queries) {
|
||||||
|
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||||
|
// If not done, then the newly created user won't be able to authenticate.
|
||||||
|
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
await masterClient.close();
|
||||||
|
await client.close();
|
||||||
|
|
||||||
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, username: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement, { noEscape: true })({
|
||||||
|
username
|
||||||
|
});
|
||||||
|
|
||||||
|
const queries = revokeStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||||
|
|
||||||
|
const client = await $getClient(providerInputs);
|
||||||
|
const masterClient = await $getClient(providerInputs, true);
|
||||||
|
|
||||||
|
// Get all processes for this login and kill them. If there are active connections to the database when drop login happens, it will throw an error.
|
||||||
|
const result = await masterClient.query<{ spid?: string }>(`sp_who '${username}'`);
|
||||||
|
|
||||||
|
if (result && result.length > 0) {
|
||||||
|
for await (const row of result) {
|
||||||
|
if (row.spid) {
|
||||||
|
await masterClient.query(`KILL ${row.spid.trim()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const query of queries) {
|
||||||
|
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
await masterClient.close();
|
||||||
|
await client.close();
|
||||||
|
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (_: unknown, username: string) => {
|
||||||
|
// No need for renewal
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
@ -32,7 +32,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
||||||
const client = hdb.createClient({
|
const client = hdb.createClient({
|
||||||
host: providerInputs.host,
|
host: providerInputs.host,
|
||||||
port: providerInputs.port,
|
port: providerInputs.port,
|
||||||
@ -64,9 +64,9 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const testResult: boolean = await new Promise((resolve, reject) => {
|
const testResult = await new Promise<boolean>((resolve, reject) => {
|
||||||
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject();
|
reject();
|
||||||
@ -86,7 +86,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
@ -114,7 +114,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, username: string) => {
|
const revoke = async (inputs: unknown, username: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||||
for await (const query of queries) {
|
for await (const query of queries) {
|
||||||
@ -135,13 +135,15 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
return { entityId: username };
|
return { entityId: username };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, username: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
|
const client = await $getClient(providerInputs);
|
||||||
try {
|
try {
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username: entityId, expiration });
|
||||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
for await (const query of queries) {
|
for await (const query of queries) {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
@ -161,7 +163,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { entityId: username };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -34,7 +34,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
|
||||||
const client = snowflake.createConnection({
|
const client = snowflake.createConnection({
|
||||||
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
|
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
|
||||||
username: providerInputs.username,
|
username: providerInputs.username,
|
||||||
@ -49,7 +49,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
let isValidConnection: boolean;
|
let isValidConnection: boolean;
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@ -107,7 +107,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
const revoke = async (inputs: unknown, username: string) => {
|
const revoke = async (inputs: unknown, username: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||||
@ -131,17 +131,16 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
return { entityId: username };
|
return { entityId: username };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, username: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
if (!providerInputs.renewStatement) return { entityId: username };
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const client = await getClient(providerInputs);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const expiration = getDaysToExpiry(new Date(expireAt));
|
const expiration = getDaysToExpiry(new Date(expireAt));
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||||
username,
|
username: entityId,
|
||||||
expiration
|
expiration
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -161,7 +160,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
client.destroy(noop);
|
client.destroy(noop);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { entityId: username };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -32,7 +32,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||||
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||||
const db = knex({
|
const db = knex({
|
||||||
client: providerInputs.client,
|
client: providerInputs.client,
|
||||||
@ -52,7 +52,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await getClient(providerInputs);
|
const db = await $getClient(providerInputs);
|
||||||
// oracle needs from keyword
|
// oracle needs from keyword
|
||||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await getClient(providerInputs);
|
const db = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername(providerInputs.client);
|
const username = generateUsername(providerInputs.client);
|
||||||
const password = generatePassword(providerInputs.client);
|
const password = generatePassword(providerInputs.client);
|
||||||
@ -90,7 +90,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await getClient(providerInputs);
|
const db = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
@ -110,13 +110,19 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await getClient(providerInputs);
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
|
const db = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
|
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||||
|
username: entityId,
|
||||||
|
expiration,
|
||||||
|
database
|
||||||
|
});
|
||||||
|
|
||||||
if (renewStatement) {
|
if (renewStatement) {
|
||||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
@ -128,7 +134,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
return { entityId: username };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
90
backend/src/ee/services/dynamic-secret/providers/totp.ts
Normal file
90
backend/src/ee/services/dynamic-secret/providers/totp.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { authenticator } from "otplib";
|
||||||
|
import { HashAlgorithms } from "otplib/core";
|
||||||
|
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
|
||||||
|
|
||||||
|
export const TotpProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs);
|
||||||
|
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async () => {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const entityId = alphaNumericNanoId(32);
|
||||||
|
const authenticatorInstance = authenticator.clone();
|
||||||
|
|
||||||
|
let secret: string;
|
||||||
|
let period: number | null | undefined;
|
||||||
|
let digits: number | null | undefined;
|
||||||
|
let algorithm: HashAlgorithms | null | undefined;
|
||||||
|
|
||||||
|
if (providerInputs.configType === TotpConfigType.URL) {
|
||||||
|
const urlObj = new URL(providerInputs.url);
|
||||||
|
secret = urlObj.searchParams.get("secret") as string;
|
||||||
|
const periodFromUrl = urlObj.searchParams.get("period");
|
||||||
|
const digitsFromUrl = urlObj.searchParams.get("digits");
|
||||||
|
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
|
||||||
|
|
||||||
|
if (periodFromUrl) {
|
||||||
|
period = +periodFromUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digitsFromUrl) {
|
||||||
|
digits = +digitsFromUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (algorithmFromUrl) {
|
||||||
|
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
secret = providerInputs.secret;
|
||||||
|
period = providerInputs.period;
|
||||||
|
digits = providerInputs.digits;
|
||||||
|
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digits) {
|
||||||
|
authenticatorInstance.options = { digits };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (algorithm) {
|
||||||
|
authenticatorInstance.options = { algorithm };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period) {
|
||||||
|
authenticatorInstance.options = { step: period };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
entityId,
|
||||||
|
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (_inputs: unknown, entityId: string) => {
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
|
// No renewal necessary
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
@ -123,7 +123,7 @@ export const groupServiceFactory = ({
|
|||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan.groups)
|
if (!plan.groups)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
|
message: "Failed to update group due to plan restriction Upgrade plan to update group."
|
||||||
});
|
});
|
||||||
|
|
||||||
const group = await groupDAL.findOne({ orgId: actorOrgId, id });
|
const group = await groupDAL.findOne({ orgId: actorOrgId, id });
|
||||||
|
58
backend/src/ee/services/hsm/hsm-fns.ts
Normal file
58
backend/src/ee/services/hsm/hsm-fns.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import * as pkcs11js from "pkcs11js";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { HsmModule } from "./hsm-types";
|
||||||
|
|
||||||
|
export const initializeHsmModule = () => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
// Create a new instance of PKCS11 module
|
||||||
|
const pkcs11 = new pkcs11js.PKCS11();
|
||||||
|
let isInitialized = false;
|
||||||
|
|
||||||
|
const initialize = () => {
|
||||||
|
if (!appCfg.isHsmConfigured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load the PKCS#11 module
|
||||||
|
pkcs11.load(appCfg.HSM_LIB_PATH!);
|
||||||
|
|
||||||
|
// Initialize the module
|
||||||
|
pkcs11.C_Initialize();
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
|
logger.info("PKCS#11 module initialized");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "Failed to initialize PKCS#11 module");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalize = () => {
|
||||||
|
if (isInitialized) {
|
||||||
|
try {
|
||||||
|
pkcs11.C_Finalize();
|
||||||
|
isInitialized = false;
|
||||||
|
logger.info("PKCS#11 module finalized");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "Failed to finalize PKCS#11 module");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModule = (): HsmModule => ({
|
||||||
|
pkcs11,
|
||||||
|
isInitialized
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialize,
|
||||||
|
finalize,
|
||||||
|
getModule
|
||||||
|
};
|
||||||
|
};
|
470
backend/src/ee/services/hsm/hsm-service.ts
Normal file
470
backend/src/ee/services/hsm/hsm-service.ts
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
import pkcs11js from "pkcs11js";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { HsmKeyType, HsmModule } from "./hsm-types";
|
||||||
|
|
||||||
|
type THsmServiceFactoryDep = {
|
||||||
|
hsmModule: HsmModule;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THsmServiceFactory = ReturnType<typeof hsmServiceFactory>;
|
||||||
|
|
||||||
|
type SyncOrAsync<T> = T | Promise<T>;
|
||||||
|
type SessionCallback<T> = (session: pkcs11js.Handle) => SyncOrAsync<T>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-empty-pattern
|
||||||
|
export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
// Constants for buffer structures
|
||||||
|
const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc
|
||||||
|
const BLOCK_SIZE = 16;
|
||||||
|
const HMAC_SIZE = 32;
|
||||||
|
|
||||||
|
const AES_KEY_SIZE = 256;
|
||||||
|
const HMAC_KEY_SIZE = 256;
|
||||||
|
|
||||||
|
const $withSession = async <T>(callbackWithSession: SessionCallback<T>): Promise<T> => {
|
||||||
|
const RETRY_INTERVAL = 200; // 200ms between attempts
|
||||||
|
const MAX_TIMEOUT = 90_000; // 90 seconds maximum total time
|
||||||
|
|
||||||
|
let sessionHandle: pkcs11js.Handle | null = null;
|
||||||
|
|
||||||
|
const removeSession = () => {
|
||||||
|
if (sessionHandle !== null) {
|
||||||
|
try {
|
||||||
|
pkcs11.C_Logout(sessionHandle);
|
||||||
|
pkcs11.C_CloseSession(sessionHandle);
|
||||||
|
logger.info("HSM: Terminated session successfully");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "HSM: Failed to terminate session");
|
||||||
|
} finally {
|
||||||
|
sessionHandle = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!pkcs11 || !isInitialized) {
|
||||||
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get slot list
|
||||||
|
let slots: pkcs11js.Handle[];
|
||||||
|
try {
|
||||||
|
slots = pkcs11.C_GetSlotList(false); // false to get all slots
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to get slot list: ${(error as Error)?.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slots.length === 0) {
|
||||||
|
throw new Error("No slots available");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appCfg.HSM_SLOT >= slots.length) {
|
||||||
|
throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotId = slots[appCfg.HSM_SLOT];
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
while (Date.now() - startTime < MAX_TIMEOUT) {
|
||||||
|
try {
|
||||||
|
// Open session
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
sessionHandle = pkcs11.C_OpenSession(slotId, pkcs11js.CKF_SERIAL_SESSION | pkcs11js.CKF_RW_SESSION);
|
||||||
|
|
||||||
|
// Login
|
||||||
|
try {
|
||||||
|
pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN);
|
||||||
|
logger.info("HSM: Successfully authenticated");
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle specific error cases
|
||||||
|
if (error instanceof pkcs11js.Pkcs11Error) {
|
||||||
|
if (error.code === pkcs11js.CKR_PIN_INCORRECT) {
|
||||||
|
// We throw instantly here to prevent further attempts, because if too many attempts are made, the HSM will potentially wipe all key material
|
||||||
|
logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`);
|
||||||
|
throw new Error("HSM: Incorrect HSM Pin detected. Please check the HSM configuration.");
|
||||||
|
}
|
||||||
|
if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) {
|
||||||
|
logger.warn("HSM: Session already logged in");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error; // Re-throw other errors
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`HSM: Session creation failed. Retrying... Error: ${(error as Error)?.message}`);
|
||||||
|
|
||||||
|
if (sessionHandle !== null) {
|
||||||
|
try {
|
||||||
|
pkcs11.C_CloseSession(sessionHandle);
|
||||||
|
} catch (closeError) {
|
||||||
|
logger.error(closeError, "HSM: Failed to close session");
|
||||||
|
}
|
||||||
|
sessionHandle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, RETRY_INTERVAL);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionHandle === null) {
|
||||||
|
throw new Error("HSM: Failed to open session after maximum retries");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute callback with session handle
|
||||||
|
const result = await callbackWithSession(sessionHandle);
|
||||||
|
removeSession();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "HSM: Failed to open session");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Ensure cleanup
|
||||||
|
removeSession();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => {
|
||||||
|
const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL;
|
||||||
|
const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES;
|
||||||
|
|
||||||
|
const template = [
|
||||||
|
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
|
||||||
|
{ type: pkcs11js.CKA_KEY_TYPE, value: keyType },
|
||||||
|
{ type: pkcs11js.CKA_LABEL, value: label }
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize search
|
||||||
|
pkcs11.C_FindObjectsInit(sessionHandle, template);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find first matching object
|
||||||
|
const handles = pkcs11.C_FindObjects(sessionHandle, 1);
|
||||||
|
|
||||||
|
if (handles.length === 0) {
|
||||||
|
throw new Error("Failed to find master key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return handles[0]; // Return the key handle
|
||||||
|
} finally {
|
||||||
|
// Always finalize the search operation
|
||||||
|
pkcs11.C_FindObjectsFinal(sessionHandle);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $keyExists = (session: pkcs11js.Handle, type: HsmKeyType): boolean => {
|
||||||
|
try {
|
||||||
|
const key = $findKey(session, type);
|
||||||
|
// items(0) will throw an error if no items are found
|
||||||
|
// Return true only if we got a valid object with handle
|
||||||
|
return !!key && key.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
// If items(0) throws, it means no key was found
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call
|
||||||
|
logger.error(error, "HSM: Failed while checking for HSM key presence");
|
||||||
|
|
||||||
|
if (error instanceof pkcs11js.Pkcs11Error) {
|
||||||
|
if (error.code === pkcs11js.CKR_OBJECT_HANDLE_INVALID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const encrypt: {
|
||||||
|
(data: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||||
|
(data: Buffer): Promise<Buffer>;
|
||||||
|
} = async (data: Buffer, providedSession?: pkcs11js.Handle) => {
|
||||||
|
if (!pkcs11 || !isInitialized) {
|
||||||
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const $performEncryption = (sessionHandle: pkcs11js.Handle) => {
|
||||||
|
try {
|
||||||
|
const aesKey = $findKey(sessionHandle, HsmKeyType.AES);
|
||||||
|
if (!aesKey) {
|
||||||
|
throw new Error("HSM: Encryption failed, AES key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC);
|
||||||
|
if (!hmacKey) {
|
||||||
|
throw new Error("HSM: Encryption failed, HMAC key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const iv = Buffer.alloc(IV_LENGTH);
|
||||||
|
pkcs11.C_GenerateRandom(sessionHandle, iv);
|
||||||
|
|
||||||
|
const encryptMechanism = {
|
||||||
|
mechanism: pkcs11js.CKM_AES_CBC_PAD,
|
||||||
|
parameter: iv
|
||||||
|
};
|
||||||
|
|
||||||
|
pkcs11.C_EncryptInit(sessionHandle, encryptMechanism, aesKey);
|
||||||
|
|
||||||
|
// Calculate max buffer size (input length + potential full block of padding)
|
||||||
|
const maxEncryptedLength = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE;
|
||||||
|
|
||||||
|
// Encrypt the data - this returns the encrypted data directly
|
||||||
|
const encryptedData = pkcs11.C_Encrypt(sessionHandle, data, Buffer.alloc(maxEncryptedLength));
|
||||||
|
|
||||||
|
// Initialize HMAC
|
||||||
|
const hmacMechanism = {
|
||||||
|
mechanism: pkcs11js.CKM_SHA256_HMAC
|
||||||
|
};
|
||||||
|
|
||||||
|
pkcs11.C_SignInit(sessionHandle, hmacMechanism, hmacKey);
|
||||||
|
|
||||||
|
// Sign the IV and encrypted data
|
||||||
|
pkcs11.C_SignUpdate(sessionHandle, iv);
|
||||||
|
pkcs11.C_SignUpdate(sessionHandle, encryptedData);
|
||||||
|
|
||||||
|
// Get the HMAC
|
||||||
|
const hmac = Buffer.alloc(HMAC_SIZE);
|
||||||
|
pkcs11.C_SignFinal(sessionHandle, hmac);
|
||||||
|
|
||||||
|
// Combine encrypted data and HMAC [Encrypted Data | HMAC]
|
||||||
|
const finalBuffer = Buffer.alloc(encryptedData.length + hmac.length);
|
||||||
|
encryptedData.copy(finalBuffer);
|
||||||
|
hmac.copy(finalBuffer, encryptedData.length);
|
||||||
|
|
||||||
|
return Buffer.concat([iv, finalBuffer]);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "HSM: Failed to perform encryption");
|
||||||
|
throw new Error(`HSM: Encryption failed: ${(error as Error)?.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (providedSession) {
|
||||||
|
return $performEncryption(providedSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await $withSession($performEncryption);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decrypt: {
|
||||||
|
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||||
|
(encryptedBlob: Buffer): Promise<Buffer>;
|
||||||
|
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
|
||||||
|
if (!pkcs11 || !isInitialized) {
|
||||||
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const $performDecryption = (sessionHandle: pkcs11js.Handle) => {
|
||||||
|
try {
|
||||||
|
// structure is: [IV (16 bytes) | Encrypted Data (N bytes) | HMAC (32 bytes)]
|
||||||
|
const iv = encryptedBlob.subarray(0, IV_LENGTH);
|
||||||
|
const encryptedDataWithHmac = encryptedBlob.subarray(IV_LENGTH);
|
||||||
|
|
||||||
|
// Split encrypted data and HMAC
|
||||||
|
const hmac = encryptedDataWithHmac.subarray(-HMAC_SIZE); // Last 32 bytes are HMAC
|
||||||
|
|
||||||
|
const encryptedData = encryptedDataWithHmac.subarray(0, -HMAC_SIZE); // Everything except last 32 bytes
|
||||||
|
|
||||||
|
// Find the keys
|
||||||
|
const aesKey = $findKey(sessionHandle, HsmKeyType.AES);
|
||||||
|
if (!aesKey) {
|
||||||
|
throw new Error("HSM: Decryption failed, AES key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC);
|
||||||
|
if (!hmacKey) {
|
||||||
|
throw new Error("HSM: Decryption failed, HMAC key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify HMAC first
|
||||||
|
const hmacMechanism = {
|
||||||
|
mechanism: pkcs11js.CKM_SHA256_HMAC
|
||||||
|
};
|
||||||
|
|
||||||
|
pkcs11.C_VerifyInit(sessionHandle, hmacMechanism, hmacKey);
|
||||||
|
pkcs11.C_VerifyUpdate(sessionHandle, iv);
|
||||||
|
pkcs11.C_VerifyUpdate(sessionHandle, encryptedData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
pkcs11.C_VerifyFinal(sessionHandle, hmac);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "HSM: HMAC verification failed");
|
||||||
|
throw new Error("HSM: Decryption failed"); // Generic error for failed verification
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only decrypt if verification passed
|
||||||
|
const decryptMechanism = {
|
||||||
|
mechanism: pkcs11js.CKM_AES_CBC_PAD,
|
||||||
|
parameter: iv
|
||||||
|
};
|
||||||
|
|
||||||
|
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
||||||
|
|
||||||
|
const tempBuffer = Buffer.alloc(encryptedData.length);
|
||||||
|
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
||||||
|
|
||||||
|
// Create a new buffer from the decrypted data
|
||||||
|
return Buffer.from(decryptedData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "HSM: Failed to perform decryption");
|
||||||
|
throw new Error("HSM: Decryption failed"); // Generic error for failed decryption, to avoid leaking details about why it failed (such as padding related errors)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (providedSession) {
|
||||||
|
return $performDecryption(providedSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await $withSession($performDecryption);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device.
|
||||||
|
const $testPkcs11Module = async (session: pkcs11js.Handle) => {
|
||||||
|
try {
|
||||||
|
if (!pkcs11 || !isInitialized) {
|
||||||
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new Error("HSM: Attempted to run test without a valid session");
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomData = pkcs11.C_GenerateRandom(session, Buffer.alloc(500));
|
||||||
|
|
||||||
|
const encryptedData = await encrypt(randomData, session);
|
||||||
|
const decryptedData = await decrypt(encryptedData, session);
|
||||||
|
|
||||||
|
const randomDataHex = randomData.toString("hex");
|
||||||
|
const decryptedDataHex = decryptedData.toString("hex");
|
||||||
|
|
||||||
|
if (randomDataHex !== decryptedDataHex && Buffer.compare(randomData, decryptedData)) {
|
||||||
|
throw new Error("HSM: Startup test failed. Decrypted data does not match original data");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "HSM: Error testing PKCS#11 module");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isActive = async () => {
|
||||||
|
if (!isInitialized || !appCfg.isHsmConfigured) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pkcs11TestPassed = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
pkcs11TestPassed = await $withSession($testPkcs11Module);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "HSM: Error testing PKCS#11 module");
|
||||||
|
}
|
||||||
|
|
||||||
|
return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startService = async () => {
|
||||||
|
if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $withSession(async (sessionHandle) => {
|
||||||
|
// Check if master key exists, create if not
|
||||||
|
|
||||||
|
const genericAttributes = [
|
||||||
|
{ type: pkcs11js.CKA_TOKEN, value: true }, // Persistent storage
|
||||||
|
{ type: pkcs11js.CKA_EXTRACTABLE, value: false }, // Cannot be extracted
|
||||||
|
{ type: pkcs11js.CKA_SENSITIVE, value: true }, // Sensitive value
|
||||||
|
{ type: pkcs11js.CKA_PRIVATE, value: true } // Requires authentication
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$keyExists(sessionHandle, HsmKeyType.AES)) {
|
||||||
|
// Template for generating 256-bit AES master key
|
||||||
|
const keyTemplate = [
|
||||||
|
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
|
||||||
|
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES },
|
||||||
|
{ type: pkcs11js.CKA_VALUE_LEN, value: AES_KEY_SIZE / 8 },
|
||||||
|
{ type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! },
|
||||||
|
{ type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption
|
||||||
|
{ type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption
|
||||||
|
...genericAttributes
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate the key
|
||||||
|
pkcs11.C_GenerateKey(
|
||||||
|
sessionHandle,
|
||||||
|
{
|
||||||
|
mechanism: pkcs11js.CKM_AES_KEY_GEN
|
||||||
|
},
|
||||||
|
keyTemplate
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`HSM: Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if HMAC key exists, create if not
|
||||||
|
if (!$keyExists(sessionHandle, HsmKeyType.HMAC)) {
|
||||||
|
const hmacKeyTemplate = [
|
||||||
|
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
|
||||||
|
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET },
|
||||||
|
{ type: pkcs11js.CKA_VALUE_LEN, value: HMAC_KEY_SIZE / 8 }, // 256-bit key
|
||||||
|
{ type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` },
|
||||||
|
{ type: pkcs11js.CKA_SIGN, value: true }, // Allow signing
|
||||||
|
{ type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification
|
||||||
|
...genericAttributes
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate the HMAC key
|
||||||
|
pkcs11.C_GenerateKey(
|
||||||
|
sessionHandle,
|
||||||
|
{
|
||||||
|
mechanism: pkcs11js.CKM_GENERIC_SECRET_KEY_GEN
|
||||||
|
},
|
||||||
|
hmacKeyTemplate
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`HSM: HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get slot info to check supported mechanisms
|
||||||
|
const slotId = pkcs11.C_GetSessionInfo(sessionHandle).slotID;
|
||||||
|
const mechanisms = pkcs11.C_GetMechanismList(slotId);
|
||||||
|
|
||||||
|
// Check for AES CBC PAD support
|
||||||
|
const hasAesCbc = mechanisms.includes(pkcs11js.CKM_AES_CBC_PAD);
|
||||||
|
|
||||||
|
if (!hasAesCbc) {
|
||||||
|
throw new Error(`Required mechanism CKM_AEC_CBC_PAD not supported by HSM`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run test encryption/decryption
|
||||||
|
const testPassed = await $testPkcs11Module(sessionHandle);
|
||||||
|
|
||||||
|
if (!testPassed) {
|
||||||
|
throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "HSM: Error initializing HSM service:");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
encrypt,
|
||||||
|
startService,
|
||||||
|
isActive,
|
||||||
|
decrypt
|
||||||
|
};
|
||||||
|
};
|
11
backend/src/ee/services/hsm/hsm-types.ts
Normal file
11
backend/src/ee/services/hsm/hsm-types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import pkcs11js from "pkcs11js";
|
||||||
|
|
||||||
|
export type HsmModule = {
|
||||||
|
pkcs11: pkcs11js.PKCS11;
|
||||||
|
isInitialized: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum HsmKeyType {
|
||||||
|
AES = "AES",
|
||||||
|
HMAC = "hmac"
|
||||||
|
}
|
@ -36,8 +36,7 @@ export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean>
|
|||||||
});
|
});
|
||||||
|
|
||||||
ldapClient.on("error", (err) => {
|
ldapClient.on("error", (err) => {
|
||||||
logger.error("LDAP client error:", err);
|
logger.error(err, "LDAP client error");
|
||||||
logger.error(err);
|
|
||||||
resolve(false);
|
resolve(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
auditLogStreams: false,
|
auditLogStreams: false,
|
||||||
auditLogStreamLimit: 3,
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
|
hsm: false,
|
||||||
oidcSSO: false,
|
oidcSSO: false,
|
||||||
scim: false,
|
scim: false,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
@ -47,7 +48,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
secretsLimit: 40
|
secretsLimit: 40
|
||||||
},
|
},
|
||||||
pkiEst: false,
|
pkiEst: false,
|
||||||
enforceMfa: false
|
enforceMfa: false,
|
||||||
|
projectTemplates: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@ -161,8 +161,8 @@ export const licenseServiceFactory = ({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`,
|
error,
|
||||||
error
|
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`
|
||||||
);
|
);
|
||||||
await keyStore.setItemWithExpiry(
|
await keyStore.setItemWithExpiry(
|
||||||
FEATURE_CACHE_KEY(orgId),
|
FEATURE_CACHE_KEY(orgId),
|
||||||
|
@ -46,6 +46,7 @@ export type TFeatureSet = {
|
|||||||
auditLogStreams: false;
|
auditLogStreams: false;
|
||||||
auditLogStreamLimit: 3;
|
auditLogStreamLimit: 3;
|
||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
|
hsm: false;
|
||||||
oidcSSO: false;
|
oidcSSO: false;
|
||||||
scim: false;
|
scim: false;
|
||||||
ldap: false;
|
ldap: false;
|
||||||
@ -65,6 +66,7 @@ export type TFeatureSet = {
|
|||||||
};
|
};
|
||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
|
projectTemplates: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
infisicalSymmetricDecrypt,
|
infisicalSymmetricDecrypt,
|
||||||
infisicalSymmetricEncypt
|
infisicalSymmetricEncypt
|
||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
@ -56,7 +56,7 @@ type TOidcConfigServiceFactoryDep = {
|
|||||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
||||||
};
|
};
|
||||||
@ -223,6 +223,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
let newUser: TUsers | undefined;
|
let newUser: TUsers | undefined;
|
||||||
|
|
||||||
if (serverCfg.trustOidcEmails) {
|
if (serverCfg.trustOidcEmails) {
|
||||||
|
// we prioritize getting the most complete user to create the new alias under
|
||||||
newUser = await userDAL.findOne(
|
newUser = await userDAL.findOne(
|
||||||
{
|
{
|
||||||
email,
|
email,
|
||||||
@ -230,6 +231,23 @@ export const oidcConfigServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!newUser) {
|
||||||
|
// this fetches user entries created via invites
|
||||||
|
newUser = await userDAL.findOne(
|
||||||
|
{
|
||||||
|
username: email
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newUser && !newUser.isEmailVerified) {
|
||||||
|
// we automatically mark it as email-verified because we've configured trust for OIDC emails
|
||||||
|
newUser = await userDAL.updateById(newUser.id, {
|
||||||
|
isEmailVerified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newUser) {
|
if (!newUser) {
|
||||||
@ -332,14 +350,20 @@ export const oidcConfigServiceFactory = ({
|
|||||||
userId: user.id
|
userId: user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService
|
||||||
template: SmtpTemplates.EmailVerification,
|
.sendMail({
|
||||||
subjectLine: "Infisical confirmation code",
|
template: SmtpTemplates.EmailVerification,
|
||||||
recipients: [user.email],
|
subjectLine: "Infisical confirmation code",
|
||||||
substitutions: {
|
recipients: [user.email],
|
||||||
code: token
|
substitutions: {
|
||||||
}
|
code: token
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
throw new OidcAuthError({
|
||||||
|
message: `Error sending email confirmation code for user registration - contact the Infisical instance admin. ${err.message}`
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isUserCompleted, providerAuthToken };
|
return { isUserCompleted, providerAuthToken };
|
||||||
@ -395,6 +419,18 @@ export const oidcConfigServiceFactory = ({
|
|||||||
message: `Organization bot for organization with ID '${org.id}' not found`,
|
message: `Organization bot for organization with ID '${org.id}' not found`,
|
||||||
name: "OrgBotNotFound"
|
name: "OrgBotNotFound"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serverCfg = await getServerCfg();
|
||||||
|
if (isActive && !serverCfg.trustOidcEmails) {
|
||||||
|
const isSmtpConnected = await smtpService.verify();
|
||||||
|
if (!isSmtpConnected) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Cannot enable OIDC when there are issues with the instance's SMTP configuration. Bypass this by turning on trust for OIDC emails in the server admin console."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const key = infisicalSymmetricDecrypt({
|
const key = infisicalSymmetricDecrypt({
|
||||||
ciphertext: orgBot.encryptedSymmetricKey,
|
ciphertext: orgBot.encryptedSymmetricKey,
|
||||||
iv: orgBot.symmetricKeyIV,
|
iv: orgBot.symmetricKeyIV,
|
||||||
|
@ -26,7 +26,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console",
|
AdminConsole = "organization-admin-console",
|
||||||
AuditLogs = "audit-logs"
|
AuditLogs = "audit-logs",
|
||||||
|
ProjectTemplates = "project-templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@ -45,6 +46,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
@ -118,6 +120,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
|
@ -127,14 +127,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
|
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
|
||||||
const docs = await db
|
const docs = await db
|
||||||
.replicaNode()(TableName.Users)
|
.replicaNode()(TableName.Users)
|
||||||
.where(`${TableName.Users}.id`, userId)
|
.where(`${TableName.Users}.id`, userId)
|
||||||
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
|
||||||
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
||||||
void queryBuilder
|
void queryBuilder
|
||||||
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
|
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||||
.andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`);
|
// @ts-expect-error akhilmhdh: this is valid knexjs query. Its just ts type argument is missing it
|
||||||
|
.andOnIn(`${TableName.GroupProjectMembership}.groupId`, subQueryUserGroups);
|
||||||
})
|
})
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.GroupProjectMembershipRole,
|
TableName.GroupProjectMembershipRole,
|
||||||
|
@ -29,4 +29,18 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isAuthMethodSaml, validateOrgSSO };
|
const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
|
||||||
|
const handler = {
|
||||||
|
get(target: Record<string, string>, prop: string) {
|
||||||
|
if (!(prop in target)) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
|
||||||
|
}
|
||||||
|
return target[prop];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Proxy(obj, handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO };
|
||||||
|
@ -21,7 +21,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
|||||||
|
|
||||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
import { TPermissionDALFactory } from "./permission-dal";
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
import { validateOrgSSO } from "./permission-fns";
|
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
|
||||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
||||||
import {
|
import {
|
||||||
buildServiceTokenProjectPermission,
|
buildServiceTokenProjectPermission,
|
||||||
@ -227,11 +227,13 @@ export const permissionServiceFactory = ({
|
|||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
const metadataKeyValuePair = objectify(
|
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||||
userProjectPermission.metadata,
|
objectify(
|
||||||
(i) => i.key,
|
userProjectPermission.metadata,
|
||||||
(i) => i.value
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const interpolateRules = templatedRules(
|
const interpolateRules = templatedRules(
|
||||||
{
|
{
|
||||||
@ -292,12 +294,15 @@ export const permissionServiceFactory = ({
|
|||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
const metadataKeyValuePair = objectify(
|
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||||
identityProjectPermission.metadata,
|
objectify(
|
||||||
(i) => i.key,
|
identityProjectPermission.metadata,
|
||||||
(i) => i.value
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const interpolateRules = templatedRules(
|
const interpolateRules = templatedRules(
|
||||||
{
|
{
|
||||||
identity: {
|
identity: {
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export enum PermissionConditionOperators {
|
import { PermissionConditionOperators } from "@app/lib/casl";
|
||||||
$IN = "$in",
|
|
||||||
$ALL = "$all",
|
|
||||||
$REGEX = "$regex",
|
|
||||||
$EQ = "$eq",
|
|
||||||
$NEQ = "$ne",
|
|
||||||
$GLOB = "$glob"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PermissionConditionSchema = {
|
export const PermissionConditionSchema = {
|
||||||
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||||
|
|
||||||
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
import { PermissionConditionSchema } from "./permission-types";
|
||||||
|
|
||||||
export enum ProjectPermissionActions {
|
export enum ProjectPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
export const ProjectTemplateDefaultEnvironments = [
|
||||||
|
{ name: "Development", slug: "dev", position: 1 },
|
||||||
|
{ name: "Staging", slug: "staging", position: 2 },
|
||||||
|
{ name: "Production", slug: "prod", position: 3 }
|
||||||
|
];
|
@ -0,0 +1,7 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectTemplateDALFactory = ReturnType<typeof projectTemplateDALFactory>;
|
||||||
|
|
||||||
|
export const projectTemplateDALFactory = (db: TDbClient) => ormify(db, TableName.ProjectTemplates);
|
@ -0,0 +1,24 @@
|
|||||||
|
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
||||||
|
import {
|
||||||
|
InfisicalProjectTemplate,
|
||||||
|
TUnpackedPermission
|
||||||
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
|
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
|
||||||
|
|
||||||
|
export const getDefaultProjectTemplate = (orgId: string) => ({
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
|
||||||
|
name: InfisicalProjectTemplate.Default,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
description: "Infisical's default project template",
|
||||||
|
environments: ProjectTemplateDefaultEnvironments,
|
||||||
|
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
permissions: permissions as TUnpackedPermission[]
|
||||||
|
})),
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
export const isInfisicalProjectTemplate = (template: string) =>
|
||||||
|
Object.values(InfisicalProjectTemplate).includes(template as InfisicalProjectTemplate);
|
@ -0,0 +1,265 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { packRules } from "@casl/ability/extra";
|
||||||
|
|
||||||
|
import { TProjectTemplates } from "@app/db/schemas";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||||
|
import {
|
||||||
|
TCreateProjectTemplateDTO,
|
||||||
|
TProjectTemplateEnvironment,
|
||||||
|
TProjectTemplateRole,
|
||||||
|
TUnpackedPermission,
|
||||||
|
TUpdateProjectTemplateDTO
|
||||||
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission";
|
||||||
|
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
|
||||||
|
|
||||||
|
import { TProjectTemplateDALFactory } from "./project-template-dal";
|
||||||
|
|
||||||
|
type TProjectTemplatesServiceFactoryDep = {
|
||||||
|
licenseService: TLicenseServiceFactory;
|
||||||
|
permissionService: TPermissionServiceFactory;
|
||||||
|
projectTemplateDAL: TProjectTemplateDALFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectTemplateServiceFactory = ReturnType<typeof projectTemplateServiceFactory>;
|
||||||
|
|
||||||
|
const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTemplates) => ({
|
||||||
|
...rest,
|
||||||
|
environments: environments as TProjectTemplateEnvironment[],
|
||||||
|
roles: [
|
||||||
|
...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
permissions: permissions as TUnpackedPermission[]
|
||||||
|
})),
|
||||||
|
...(roles as TProjectTemplateRole[]).map((role) => ({
|
||||||
|
...role,
|
||||||
|
permissions: unpackPermissions(role.permissions)
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const projectTemplateServiceFactory = ({
|
||||||
|
licenseService,
|
||||||
|
permissionService,
|
||||||
|
projectTemplateDAL
|
||||||
|
}: TProjectTemplatesServiceFactoryDep) => {
|
||||||
|
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
const projectTemplates = await projectTemplateDAL.find({
|
||||||
|
orgId: actor.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
getDefaultProjectTemplate(actor.orgId),
|
||||||
|
...projectTemplates.map((template) => $unpackProjectTemplate(template))
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const findProjectTemplateByName = async (name: string, actor: OrgServiceActor) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectTemplate = await projectTemplateDAL.findOne({ name, orgId: actor.orgId });
|
||||||
|
|
||||||
|
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectTemplate.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...$unpackProjectTemplate(projectTemplate),
|
||||||
|
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const findProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectTemplate = await projectTemplateDAL.findById(id);
|
||||||
|
|
||||||
|
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectTemplate.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...$unpackProjectTemplate(projectTemplate),
|
||||||
|
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createProjectTemplate = async (
|
||||||
|
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await projectTemplateDAL.findOne({
|
||||||
|
name: params.name,
|
||||||
|
orgId: actor.orgId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A project template with the name "${params.name}" already exists.`
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectTemplate = await projectTemplateDAL.create({
|
||||||
|
...params,
|
||||||
|
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
|
||||||
|
environments: JSON.stringify(environments),
|
||||||
|
orgId: actor.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return $unpackProjectTemplate(projectTemplate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProjectTemplateById = async (
|
||||||
|
id: string,
|
||||||
|
{ roles, environments, ...params }: TUpdateProjectTemplateDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to update project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectTemplate = await projectTemplateDAL.findById(id);
|
||||||
|
|
||||||
|
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectTemplate.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
if (params.name && projectTemplate.name !== params.name) {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await projectTemplateDAL.findOne({
|
||||||
|
name: params.name,
|
||||||
|
orgId: projectTemplate.orgId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A project template with the name "${params.name}" already exists.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProjectTemplate = await projectTemplateDAL.updateById(id, {
|
||||||
|
...params,
|
||||||
|
roles: roles
|
||||||
|
? JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) })))
|
||||||
|
: undefined,
|
||||||
|
environments: environments ? JSON.stringify(environments) : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
return $unpackProjectTemplate(updatedProjectTemplate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to delete project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectTemplate = await projectTemplateDAL.findById(id);
|
||||||
|
|
||||||
|
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectTemplate.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
const deletedProjectTemplate = await projectTemplateDAL.deleteById(id);
|
||||||
|
|
||||||
|
return $unpackProjectTemplate(deletedProjectTemplate);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listProjectTemplatesByOrg,
|
||||||
|
createProjectTemplate,
|
||||||
|
updateProjectTemplateById,
|
||||||
|
deleteProjectTemplateById,
|
||||||
|
findProjectTemplateById,
|
||||||
|
findProjectTemplateByName
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TProjectEnvironments } from "@app/db/schemas";
|
||||||
|
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||||
|
|
||||||
|
export type TProjectTemplateEnvironment = Pick<TProjectEnvironments, "name" | "slug" | "position">;
|
||||||
|
|
||||||
|
export type TProjectTemplateRole = {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
permissions: TProjectPermissionV2Schema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateProjectTemplateDTO = {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
roles: TProjectTemplateRole[];
|
||||||
|
environments: TProjectTemplateEnvironment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
|
||||||
|
|
||||||
|
export type TUnpackedPermission = z.infer<typeof UnpackedPermissionSchema>;
|
||||||
|
|
||||||
|
export enum InfisicalProjectTemplate {
|
||||||
|
Default = "default"
|
||||||
|
}
|
@ -46,7 +46,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
|
|||||||
}
|
}
|
||||||
return rateLimit;
|
return rateLimit;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Error fetching rate limits %o", err);
|
logger.error(err, "Error fetching rate limits");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -69,12 +69,12 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
|
|||||||
mfaRateLimit: rateLimit.mfaRateLimit
|
mfaRateLimit: rateLimit.mfaRateLimit
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
|
logger.info(newRateLimitMaxConfiguration, "syncRateLimitConfiguration: rate limit configuration");
|
||||||
Object.freeze(newRateLimitMaxConfiguration);
|
Object.freeze(newRateLimitMaxConfiguration);
|
||||||
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
|
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error syncing rate limit configurations: %o`, error);
|
logger.error(error, "Error syncing rate limit configurations");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -238,11 +238,11 @@ export const secretScanningQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => {
|
queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => {
|
||||||
logger.error("Failed to secret scan on push", job?.data, err);
|
logger.error(err, "Failed to secret scan on push", job?.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
queueService.listen(QueueName.SecretFullRepoScan, "failed", (job, err) => {
|
queueService.listen(QueueName.SecretFullRepoScan, "failed", (job, err) => {
|
||||||
logger.error("Failed to do full repo secret scan", job?.data, err);
|
logger.error(err, "Failed to do full repo secret scan", job?.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { startFullRepoScan, startPushEventScan };
|
return { startFullRepoScan, startPushEventScan };
|
||||||
|
@ -391,7 +391,9 @@ export const PROJECTS = {
|
|||||||
CREATE: {
|
CREATE: {
|
||||||
organizationSlug: "The slug of the organization to create the project in.",
|
organizationSlug: "The slug of the organization to create the project in.",
|
||||||
projectName: "The name of the project to create.",
|
projectName: "The name of the project to create.",
|
||||||
slug: "An optional slug for the project."
|
projectDescription: "An optional description label for the project.",
|
||||||
|
slug: "An optional slug for the project.",
|
||||||
|
template: "The name of the project template, if specified, to apply to this project."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
workspaceId: "The ID of the project to delete."
|
workspaceId: "The ID of the project to delete."
|
||||||
@ -402,6 +404,7 @@ export const PROJECTS = {
|
|||||||
UPDATE: {
|
UPDATE: {
|
||||||
workspaceId: "The ID of the project to update.",
|
workspaceId: "The ID of the project to update.",
|
||||||
name: "The new name of the project.",
|
name: "The new name of the project.",
|
||||||
|
projectDescription: "An optional description label for the project.",
|
||||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
||||||
},
|
},
|
||||||
GET_KEY: {
|
GET_KEY: {
|
||||||
@ -1438,3 +1441,22 @@ export const KMS = {
|
|||||||
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ProjectTemplates = {
|
||||||
|
CREATE: {
|
||||||
|
name: "The name of the project template to be created. Must be slug-friendly.",
|
||||||
|
description: "An optional description of the project template.",
|
||||||
|
roles: "The roles to be created when the template is applied to a project.",
|
||||||
|
environments: "The environments to be created when the template is applied to a project."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
templateId: "The ID of the project template to be updated.",
|
||||||
|
name: "The updated name of the project template. Must be slug-friendly.",
|
||||||
|
description: "The updated description of the project template.",
|
||||||
|
roles: "The updated roles to be created when the template is applied to a project.",
|
||||||
|
environments: "The updated environments to be created when the template is applied to a project."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
templateId: "The ID of the project template to be deleted."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -54,3 +54,12 @@ export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2:
|
|||||||
|
|
||||||
return set1.size >= set2.size;
|
return set1.size >= set2.size;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum PermissionConditionOperators {
|
||||||
|
$IN = "$in",
|
||||||
|
$ALL = "$all",
|
||||||
|
$REGEX = "$regex",
|
||||||
|
$EQ = "$eq",
|
||||||
|
$NEQ = "$ne",
|
||||||
|
$GLOB = "$glob"
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Logger } from "pino";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { removeTrailingSlash } from "../fn";
|
import { removeTrailingSlash } from "../fn";
|
||||||
|
import { CustomLogger } from "../logger/logger";
|
||||||
import { zpStr } from "../zod";
|
import { zpStr } from "../zod";
|
||||||
|
|
||||||
export const GITLAB_URL = "https://gitlab.com";
|
export const GITLAB_URL = "https://gitlab.com";
|
||||||
@ -157,16 +157,37 @@ const envSchema = z
|
|||||||
INFISICAL_CLOUD: zodStrBool.default("false"),
|
INFISICAL_CLOUD: zodStrBool.default("false"),
|
||||||
MAINTENANCE_MODE: zodStrBool.default("false"),
|
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// TELEMETRY
|
||||||
|
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
|
||||||
|
OTEL_EXPORT_OTLP_ENDPOINT: zpStr(z.string().optional()),
|
||||||
|
OTEL_OTLP_PUSH_INTERVAL: z.coerce.number().default(30000),
|
||||||
|
OTEL_COLLECTOR_BASIC_AUTH_USERNAME: zpStr(z.string().optional()),
|
||||||
|
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD: zpStr(z.string().optional()),
|
||||||
|
OTEL_EXPORT_TYPE: z.enum(["prometheus", "otlp"]).optional(),
|
||||||
|
|
||||||
PLAIN_API_KEY: zpStr(z.string().optional()),
|
PLAIN_API_KEY: zpStr(z.string().optional()),
|
||||||
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
|
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
|
||||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
||||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true")
|
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
|
||||||
|
|
||||||
|
// HSM
|
||||||
|
HSM_LIB_PATH: zpStr(z.string().optional()),
|
||||||
|
HSM_PIN: zpStr(z.string().optional()),
|
||||||
|
HSM_KEY_LABEL: zpStr(z.string().optional()),
|
||||||
|
HSM_SLOT: z.coerce.number().optional().default(0)
|
||||||
})
|
})
|
||||||
|
// To ensure that basic encryption is always possible.
|
||||||
|
.refine(
|
||||||
|
(data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY),
|
||||||
|
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
|
||||||
|
)
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
|
|
||||||
DB_READ_REPLICAS: data.DB_READ_REPLICAS
|
DB_READ_REPLICAS: data.DB_READ_REPLICAS
|
||||||
? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS))
|
? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS))
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -175,10 +196,14 @@ const envSchema = z
|
|||||||
isRedisConfigured: Boolean(data.REDIS_URL),
|
isRedisConfigured: Boolean(data.REDIS_URL),
|
||||||
isDevelopmentMode: data.NODE_ENV === "development",
|
isDevelopmentMode: data.NODE_ENV === "development",
|
||||||
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
|
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
|
||||||
|
|
||||||
isSecretScanningConfigured:
|
isSecretScanningConfigured:
|
||||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
||||||
|
isHsmConfigured:
|
||||||
|
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
|
||||||
|
|
||||||
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
|
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
|
||||||
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
|
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
|
||||||
}));
|
}));
|
||||||
@ -187,11 +212,11 @@ let envCfg: Readonly<z.infer<typeof envSchema>>;
|
|||||||
|
|
||||||
export const getConfig = () => envCfg;
|
export const getConfig = () => envCfg;
|
||||||
// cannot import singleton logger directly as it needs config to load various transport
|
// cannot import singleton logger directly as it needs config to load various transport
|
||||||
export const initEnvConfig = (logger: Logger) => {
|
export const initEnvConfig = (logger?: CustomLogger) => {
|
||||||
const parsedEnv = envSchema.safeParse(process.env);
|
const parsedEnv = envSchema.safeParse(process.env);
|
||||||
if (!parsedEnv.success) {
|
if (!parsedEnv.success) {
|
||||||
logger.error("Invalid environment variables. Check the error below");
|
(logger ?? console).error("Invalid environment variables. Check the error below");
|
||||||
logger.error(parsedEnv.error.issues);
|
(logger ?? console).error(parsedEnv.error.issues);
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,3 +133,15 @@ export class ScimRequestError extends Error {
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class OidcAuthError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
|
||||||
|
super(message || "Something went wrong");
|
||||||
|
this.name = name || "OidcAuthError";
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
// logger follows a singleton pattern
|
// logger follows a singleton pattern
|
||||||
// easier to use it that's all.
|
// easier to use it that's all.
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import pino, { Logger } from "pino";
|
import pino, { Logger } from "pino";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -13,14 +15,37 @@ const logLevelToSeverityLookup: Record<string, string> = {
|
|||||||
"60": "CRITICAL"
|
"60": "CRITICAL"
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-mutable-exports
|
|
||||||
export let logger: Readonly<Logger>;
|
|
||||||
// akhilmhdh:
|
// akhilmhdh:
|
||||||
// The logger is not placed in the main app config to avoid a circular dependency.
|
// The logger is not placed in the main app config to avoid a circular dependency.
|
||||||
// The config requires the logger to display errors when an invalid environment is supplied.
|
// The config requires the logger to display errors when an invalid environment is supplied.
|
||||||
// On the other hand, the logger needs the config to obtain credentials for AWS or other transports.
|
// On the other hand, the logger needs the config to obtain credentials for AWS or other transports.
|
||||||
// By keeping the logger separate, it becomes an independent package.
|
// By keeping the logger separate, it becomes an independent package.
|
||||||
|
|
||||||
|
// We define our own custom logger interface to enforce structure to the logging methods.
|
||||||
|
|
||||||
|
export interface CustomLogger extends Omit<Logger, "info" | "error" | "warn" | "debug"> {
|
||||||
|
info: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(obj: unknown, msg?: string, ...args: any[]): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
error: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(obj: unknown, msg?: string, ...args: any[]): void;
|
||||||
|
};
|
||||||
|
warn: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(obj: unknown, msg?: string, ...args: any[]): void;
|
||||||
|
};
|
||||||
|
debug: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(obj: unknown, msg?: string, ...args: any[]): void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-mutable-exports
|
||||||
|
export let logger: Readonly<CustomLogger>;
|
||||||
|
|
||||||
const loggerConfig = z.object({
|
const loggerConfig = z.object({
|
||||||
AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"),
|
AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"),
|
||||||
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
|
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
|
||||||
@ -62,6 +87,17 @@ const redactedKeys = [
|
|||||||
"config"
|
"config"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID";
|
||||||
|
|
||||||
|
const extractRequestId = () => {
|
||||||
|
try {
|
||||||
|
return requestContext.get("requestId") || UNKNOWN_REQUEST_ID;
|
||||||
|
} catch (err) {
|
||||||
|
console.log("failed to get request context", err);
|
||||||
|
return UNKNOWN_REQUEST_ID;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const initLogger = async () => {
|
export const initLogger = async () => {
|
||||||
const cfg = loggerConfig.parse(process.env);
|
const cfg = loggerConfig.parse(process.env);
|
||||||
const targets: pino.TransportMultiOptions["targets"][number][] = [
|
const targets: pino.TransportMultiOptions["targets"][number][] = [
|
||||||
@ -94,6 +130,30 @@ export const initLogger = async () => {
|
|||||||
targets
|
targets
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const wrapLogger = (originalLogger: Logger): CustomLogger => {
|
||||||
|
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
|
||||||
|
originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => {
|
||||||
|
return originalLogger.child({ requestId: extractRequestId() }).info(obj, msg, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
|
||||||
|
originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => {
|
||||||
|
return originalLogger.child({ requestId: extractRequestId() }).error(obj, msg, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
|
||||||
|
originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => {
|
||||||
|
return originalLogger.child({ requestId: extractRequestId() }).warn(obj, msg, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
|
||||||
|
originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => {
|
||||||
|
return originalLogger.child({ requestId: extractRequestId() }).debug(obj, msg, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
return originalLogger;
|
||||||
|
};
|
||||||
|
|
||||||
logger = pino(
|
logger = pino(
|
||||||
{
|
{
|
||||||
mixin(_context, level) {
|
mixin(_context, level) {
|
||||||
@ -113,5 +173,6 @@ export const initLogger = async () => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
transport
|
transport
|
||||||
);
|
);
|
||||||
return logger;
|
|
||||||
|
return wrapLogger(logger);
|
||||||
};
|
};
|
||||||
|
91
backend/src/lib/telemetry/instrumentation.ts
Normal file
91
backend/src/lib/telemetry/instrumentation.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import opentelemetry, { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
||||||
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||||
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
||||||
|
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
||||||
|
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
||||||
|
import { Resource } from "@opentelemetry/resources";
|
||||||
|
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
||||||
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
import { initEnvConfig } from "../config/env";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const initTelemetryInstrumentation = ({
|
||||||
|
exportType,
|
||||||
|
otlpURL,
|
||||||
|
otlpUser,
|
||||||
|
otlpPassword,
|
||||||
|
otlpPushInterval
|
||||||
|
}: {
|
||||||
|
exportType?: string;
|
||||||
|
otlpURL?: string;
|
||||||
|
otlpUser?: string;
|
||||||
|
otlpPassword?: string;
|
||||||
|
otlpPushInterval?: number;
|
||||||
|
}) => {
|
||||||
|
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
|
||||||
|
|
||||||
|
const resource = Resource.default().merge(
|
||||||
|
new Resource({
|
||||||
|
[ATTR_SERVICE_NAME]: "infisical-core",
|
||||||
|
[ATTR_SERVICE_VERSION]: "0.1.0"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const metricReaders = [];
|
||||||
|
switch (exportType) {
|
||||||
|
case "prometheus": {
|
||||||
|
const promExporter = new PrometheusExporter();
|
||||||
|
metricReaders.push(promExporter);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "otlp": {
|
||||||
|
const otlpExporter = new OTLPMetricExporter({
|
||||||
|
url: `${otlpURL}/v1/metrics`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${btoa(`${otlpUser}:${otlpPassword}`)}`
|
||||||
|
},
|
||||||
|
temporalityPreference: AggregationTemporality.DELTA
|
||||||
|
});
|
||||||
|
metricReaders.push(
|
||||||
|
new PeriodicExportingMetricReader({
|
||||||
|
exporter: otlpExporter,
|
||||||
|
exportIntervalMillis: otlpPushInterval
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid OTEL export type");
|
||||||
|
}
|
||||||
|
|
||||||
|
const meterProvider = new MeterProvider({
|
||||||
|
resource,
|
||||||
|
readers: metricReaders
|
||||||
|
});
|
||||||
|
|
||||||
|
opentelemetry.metrics.setGlobalMeterProvider(meterProvider);
|
||||||
|
|
||||||
|
registerInstrumentations({
|
||||||
|
instrumentations: [getNodeAutoInstrumentations()]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupTelemetry = () => {
|
||||||
|
const appCfg = initEnvConfig();
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
console.log("Initializing telemetry instrumentation");
|
||||||
|
initTelemetryInstrumentation({
|
||||||
|
otlpURL: appCfg.OTEL_EXPORT_OTLP_ENDPOINT,
|
||||||
|
otlpUser: appCfg.OTEL_COLLECTOR_BASIC_AUTH_USERNAME,
|
||||||
|
otlpPassword: appCfg.OTEL_COLLECTOR_BASIC_AUTH_PASSWORD,
|
||||||
|
otlpPushInterval: appCfg.OTEL_OTLP_PUSH_INTERVAL,
|
||||||
|
exportType: appCfg.OTEL_EXPORT_TYPE
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setupTelemetry();
|
@ -58,7 +58,7 @@ export enum OrderByDirection {
|
|||||||
DESC = "desc"
|
DESC = "desc"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectServiceActor = {
|
export type OrgServiceActor = {
|
||||||
type: ActorType;
|
type: ActorType;
|
||||||
id: string;
|
id: string;
|
||||||
authMethod: ActorAuthMethod;
|
authMethod: ActorAuthMethod;
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export { isDisposableEmail } from "./validate-email";
|
export { isDisposableEmail } from "./validate-email";
|
||||||
|
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
|
||||||
export { blockLocalAndPrivateIpAddresses } from "./validate-url";
|
export { blockLocalAndPrivateIpAddresses } from "./validate-url";
|
||||||
|
8
backend/src/lib/validator/validate-folder-name.ts
Normal file
8
backend/src/lib/validator/validate-folder-name.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// regex to allow only alphanumeric, dash, underscore
|
||||||
|
export const isValidFolderName = (name: string) => /^[a-zA-Z0-9-_]+$/.test(name);
|
||||||
|
|
||||||
|
export const isValidSecretPath = (path: string) =>
|
||||||
|
path
|
||||||
|
.split("/")
|
||||||
|
.filter((el) => el.length)
|
||||||
|
.every((name) => isValidFolderName(name));
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user