mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
301 Commits
databricks
...
misc/add-u
Author | SHA1 | Date | |
---|---|---|---|
|
045debeaf3 | ||
|
3fb8ad2fac | ||
|
58ebebb162 | ||
|
b7640f2d03 | ||
|
2ee4d68fd0 | ||
|
3ca931acf1 | ||
|
8e311658d4 | ||
|
9116acd37b | ||
|
0513307d98 | ||
|
efc3b6d474 | ||
|
07e1d1b130 | ||
|
7f76779124 | ||
|
30bcf1f204 | ||
|
706feafbf2 | ||
|
fc4e3f1f72 | ||
|
dcd5f20325 | ||
|
58f3e116a3 | ||
|
7bc5aad8ec | ||
|
a16dc3aef6 | ||
|
da7746c639 | ||
|
cd5b6da541 | ||
|
2dda7180a9 | ||
|
30ccfbfc8e | ||
|
aa76924ee6 | ||
|
d8f679e72d | ||
|
bf6cfbac7a | ||
|
8e82813894 | ||
|
df21a1fb81 | ||
|
bdbb6346cb | ||
|
ea9da6d2a8 | ||
|
3c2c70912f | ||
|
b607429b99 | ||
|
16c1516979 | ||
|
f5dbbaf1fd | ||
|
2a292455ef | ||
|
4d040706a9 | ||
|
5183f76397 | ||
|
4b3efb43b0 | ||
|
96046726b2 | ||
|
a86a951acc | ||
|
5e70860160 | ||
|
abbd427ee2 | ||
|
8fd5fdbc6a | ||
|
77e1ccc8d7 | ||
|
711cc438f6 | ||
|
8447190bf8 | ||
|
12b447425b | ||
|
9cb1a31287 | ||
|
b00413817d | ||
|
2a8bd74e88 | ||
|
f28f4f7561 | ||
|
f0b05c683b | ||
|
3e8f02a4f9 | ||
|
50ee60a3ea | ||
|
21bdecdf2a | ||
|
bf09461416 | ||
|
1ff615913c | ||
|
281cedf1a2 | ||
|
a8d847f139 | ||
|
2a0c0590f1 | ||
|
2e6d525d27 | ||
|
7fd4249d00 | ||
|
90cfc44592 | ||
|
8c403780c2 | ||
|
b69c091f2f | ||
|
4a66395ce6 | ||
|
8c18753e3f | ||
|
85c5d69c36 | ||
|
94fe577046 | ||
|
a0a579834c | ||
|
b5575f4c20 | ||
|
f98f212ecf | ||
|
b331a4a708 | ||
|
e351a16b5a | ||
|
2cfca823f2 | ||
|
a8398a7009 | ||
|
8c054cedfc | ||
|
24d4f8100c | ||
|
08f23e2d3c | ||
|
d1ad605ac4 | ||
|
9dd5857ff5 | ||
|
babbacdc96 | ||
|
76427f43f7 | ||
|
3badcea95b | ||
|
1a4c0fe8d9 | ||
|
04f6864abc | ||
|
fcbe0f59d2 | ||
|
e95b6fdeaa | ||
|
5391bcd3b2 | ||
|
48fd9e2a56 | ||
|
7b5926d865 | ||
|
034123bcdf | ||
|
f3786788fd | ||
|
c406f6d78d | ||
|
eb66295dd4 | ||
|
798215e84c | ||
|
53f7491441 | ||
|
53f6ab118b | ||
|
0f5a1b13a6 | ||
|
5c606fe45f | ||
|
bbf60169eb | ||
|
e004be22e3 | ||
|
016cb4a7ba | ||
|
9bfc2a5dd2 | ||
|
72dbef97fb | ||
|
f376eaae13 | ||
|
026f883d21 | ||
|
e42f860261 | ||
|
08ec8c9b73 | ||
|
1512d4f496 | ||
|
9f7b42ad91 | ||
|
3045477c32 | ||
|
be4adc2759 | ||
|
4eba80905a | ||
|
b023bc7442 | ||
|
a0029ab469 | ||
|
53605c3880 | ||
|
e5bca5b5df | ||
|
4091bc19e9 | ||
|
23bd048bb9 | ||
|
17a4674821 | ||
|
ec9631107d | ||
|
3fa450b9a7 | ||
|
3b9c62c366 | ||
|
cb3d171d48 | ||
|
c29841fbcf | ||
|
fcccf1bd8d | ||
|
4382825162 | ||
|
f80ef1dcc8 | ||
|
7abf3e3642 | ||
|
82ef35bd08 | ||
|
4eb668b5a5 | ||
|
18edea9f26 | ||
|
787c091948 | ||
|
ff269b1063 | ||
|
ca0636cb25 | ||
|
b995358b7e | ||
|
7aaf0f4ed3 | ||
|
68646bcdf8 | ||
|
9989ceb6d1 | ||
|
95d7ba5f22 | ||
|
2aa6fdf983 | ||
|
be5a32a5d6 | ||
|
f009cd329b | ||
|
e2778864e2 | ||
|
ea7375b2c6 | ||
|
d42566c335 | ||
|
45cbd9f006 | ||
|
8580602ea7 | ||
|
7ff75cdfab | ||
|
bd8c8871c0 | ||
|
d5aa13b277 | ||
|
428dc5d371 | ||
|
f1facf1f2c | ||
|
31dc36d4e2 | ||
|
51f29e5357 | ||
|
30f0f174d1 | ||
|
3e7110f334 | ||
|
e6af7a6fb9 | ||
|
de420fd02c | ||
|
41a3ca149d | ||
|
da38d1a261 | ||
|
b0d8c8fb23 | ||
|
d84bac5fba | ||
|
44f74e4d12 | ||
|
c16a4e00d8 | ||
|
11f2719842 | ||
|
f8153dd896 | ||
|
b104f8c07d | ||
|
746687e5b5 | ||
|
080b1e1550 | ||
|
38a6fd140c | ||
|
19d66abc38 | ||
|
e61c0be6db | ||
|
917573931f | ||
|
929a41065c | ||
|
9b44972e77 | ||
|
17e576511b | ||
|
afd444cad6 | ||
|
55b1fbdf52 | ||
|
46ca5c8efa | ||
|
f7406ea8f8 | ||
|
f34370cb9d | ||
|
78718cd299 | ||
|
1307fa49d4 | ||
|
a7ca242f5d | ||
|
c6b3b24312 | ||
|
8520029958 | ||
|
7905017121 | ||
|
4bbe80c083 | ||
|
d65ae2c61b | ||
|
84c534ef70 | ||
|
ce4c5d8ea1 | ||
|
617aa2f533 | ||
|
e9dd3340bf | ||
|
1c2b4e91ba | ||
|
fb030401ab | ||
|
f4bd48fd1d | ||
|
177ccf6c9e | ||
|
9200137d6c | ||
|
a196028064 | ||
|
0c0e20f00e | ||
|
710429c805 | ||
|
c121bd930b | ||
|
87d383a9c4 | ||
|
6e590a78a0 | ||
|
ab4b6c17b3 | ||
|
27cd40c8ce | ||
|
5f089e0b9d | ||
|
19940522aa | ||
|
28b18c1cb1 | ||
|
7ae2cc2db8 | ||
|
97c069bc0f | ||
|
4a51b4d619 | ||
|
478e0c5ff5 | ||
|
5c08136fca | ||
|
cb8528adc4 | ||
|
d7935d30ce | ||
|
ac3bab3074 | ||
|
4a44b7857e | ||
|
63b8301065 | ||
|
babe70e00f | ||
|
2ba834b036 | ||
|
db7a6f3530 | ||
|
f23ea0991c | ||
|
d80a104f7b | ||
|
f8ab2bcdfd | ||
|
d980d471e8 | ||
|
9cdb4dcde9 | ||
|
3583a09ab5 | ||
|
86b6d23f34 | ||
|
2c31ac0569 | ||
|
d6c1b8e30c | ||
|
0d4d73b61d | ||
|
198b607e2e | ||
|
f0e6bcef9b | ||
|
69fb87bbfc | ||
|
b0cd5bd10d | ||
|
15119ffda9 | ||
|
4df409e627 | ||
|
3a5a88467d | ||
|
012f265363 | ||
|
823bf134dd | ||
|
1f5a73047d | ||
|
0366df6e19 | ||
|
c77e0c0666 | ||
|
8e70731c4c | ||
|
1db8c9ea29 | ||
|
21c6700db2 | ||
|
619062033b | ||
|
92b3b9157a | ||
|
f6cd78e078 | ||
|
36973b1b5c | ||
|
1ca578ee03 | ||
|
8a7f7ac9fd | ||
|
049fd8e769 | ||
|
8e2cce865a | ||
|
943c2b0e69 | ||
|
8518fed726 | ||
|
5148435f5f | ||
|
2c825616a6 | ||
|
febbd4ade5 | ||
|
603b740bbe | ||
|
874dc01692 | ||
|
b44b8bf647 | ||
|
258e561b84 | ||
|
5802638fc4 | ||
|
e2e7004583 | ||
|
7826324435 | ||
|
af652f7e52 | ||
|
2bfc1caec5 | ||
|
4b9e3e44e2 | ||
|
b2a680ebd7 | ||
|
b269bb81fe | ||
|
5ca7ff4f2d | ||
|
ec12d57862 | ||
|
2d16f5f258 | ||
|
93912da528 | ||
|
ffc5e61faa | ||
|
70e68f4441 | ||
|
a004934a28 | ||
|
0811192eed | ||
|
1e09487572 | ||
|
86202caa95 | ||
|
285fca4ded | ||
|
b8a07979c3 | ||
|
292c9051bd | ||
|
da35ec90bc | ||
|
77fac45df1 | ||
|
0ab90383c2 | ||
|
a3acfa65a2 | ||
|
0269f57768 | ||
|
9f9ded5102 | ||
|
8b315c946c | ||
|
dd9a7755bc | ||
|
64c2fba350 | ||
|
c7f80f7d9e | ||
|
ecf2cb6e51 | ||
|
1e5a9a6020 | ||
|
00e69e6632 | ||
|
cedb22a39a |
@@ -112,4 +112,11 @@ INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
|||||||
|
|
||||||
# azure app connection
|
# azure app connection
|
||||||
INF_APP_CONNECTION_AZURE_CLIENT_ID=
|
INF_APP_CONNECTION_AZURE_CLIENT_ID=
|
||||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
|
INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# datadog
|
||||||
|
SHOULD_USE_DATADOG_TRACER=
|
||||||
|
DATADOG_PROFILING_ENABLED=
|
||||||
|
DATADOG_ENV=
|
||||||
|
DATADOG_SERVICE=
|
||||||
|
DATADOG_HOSTNAME=
|
||||||
|
@@ -32,10 +32,23 @@ jobs:
|
|||||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||||
- name: Start the server
|
- name: Start the server
|
||||||
run: |
|
run: |
|
||||||
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
||||||
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
||||||
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
||||||
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET -e ENCRYPTION_KEY=$ENCRYPTION_KEY --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
|
|
||||||
|
echo "Examining built image:"
|
||||||
|
docker image inspect infisical-api | grep -A 5 "Entrypoint"
|
||||||
|
|
||||||
|
docker run --name infisical-api -d -p 4000:4000 \
|
||||||
|
-e DB_CONNECTION_URI=$DB_CONNECTION_URI \
|
||||||
|
-e REDIS_URL=$REDIS_URL \
|
||||||
|
-e JWT_AUTH_SECRET=$JWT_AUTH_SECRET \
|
||||||
|
-e ENCRYPTION_KEY=$ENCRYPTION_KEY \
|
||||||
|
--env-file .env \
|
||||||
|
infisical-api
|
||||||
|
|
||||||
|
echo "Container status right after creation:"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
env:
|
env:
|
||||||
REDIS_URL: redis://172.17.0.1:6379
|
REDIS_URL: redis://172.17.0.1:6379
|
||||||
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
|
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
|
||||||
@@ -43,27 +56,39 @@ jobs:
|
|||||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21.5'
|
go-version: "1.21.5"
|
||||||
- name: Wait for container to be stable and check logs
|
- name: Wait for container to be stable and check logs
|
||||||
run: |
|
run: |
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
HEALTHY=0
|
HEALTHY=0
|
||||||
while [ $SECONDS -lt 60 ]; do
|
while [ $SECONDS -lt 60 ]; do
|
||||||
if docker ps | grep infisical-api | grep -q healthy; then
|
# Check if container is running
|
||||||
echo "Container is healthy."
|
if docker ps | grep infisical-api; then
|
||||||
HEALTHY=1
|
# Try to access the API endpoint
|
||||||
|
if curl -s -f http://localhost:4000/api/docs/json > /dev/null 2>&1; then
|
||||||
|
echo "API endpoint is responding. Container seems healthy."
|
||||||
|
HEALTHY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Container is not running!"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
|
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
|
||||||
|
sleep 5
|
||||||
docker logs infisical-api
|
SECONDS=$((SECONDS+5))
|
||||||
|
|
||||||
sleep 2
|
|
||||||
SECONDS=$((SECONDS+2))
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ $HEALTHY -ne 1 ]; then
|
if [ $HEALTHY -ne 1 ]; then
|
||||||
echo "Container did not become healthy in time"
|
echo "Container did not become healthy in time"
|
||||||
|
echo "Container status:"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
|
echo "Container logs (if any):"
|
||||||
|
docker logs infisical-api || echo "No logs available"
|
||||||
|
echo "Container inspection:"
|
||||||
|
docker inspect infisical-api | grep -A 5 "State"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- name: Install openapi-diff
|
- name: Install openapi-diff
|
||||||
@@ -71,7 +96,8 @@ jobs:
|
|||||||
- name: Running OpenAPI Spec diff action
|
- name: Running OpenAPI Spec diff action
|
||||||
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
docker compose -f "docker-compose.dev.yml" down
|
docker compose -f "docker-compose.dev.yml" down
|
||||||
docker stop infisical-api
|
docker stop infisical-api || true
|
||||||
docker remove infisical-api
|
docker rm infisical-api || true
|
@@ -26,7 +26,7 @@ jobs:
|
|||||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||||
|
|
||||||
npm-release:
|
npm-release:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
working-directory: ./npm
|
working-directory: ./npm
|
||||||
needs:
|
needs:
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
goreleaser:
|
goreleaser:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
needs: [cli-integration-tests]
|
needs: [cli-integration-tests]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -103,11 +103,12 @@ jobs:
|
|||||||
go-version: ">=1.19.3"
|
go-version: ">=1.19.3"
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: cli/go.sum
|
cache-dependency-path: cli/go.sum
|
||||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
- name: Setup for libssl1.0-dev
|
||||||
run: |
|
run: |
|
||||||
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||||
sudo apt update && apt-cache policy libssl1.0-dev
|
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
|
||||||
sudo apt-get install libssl1.0-dev
|
sudo apt update
|
||||||
|
sudo apt-get install -y libssl1.0-dev
|
||||||
- name: OSXCross for CGO Support
|
- name: OSXCross for CGO Support
|
||||||
run: |
|
run: |
|
||||||
mkdir ../../osxcross
|
mkdir ../../osxcross
|
||||||
|
@@ -161,6 +161,9 @@ COPY --from=backend-runner /app /backend
|
|||||||
|
|
||||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||||
|
|
||||||
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
|
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
|
|
||||||
ENV PORT 8080
|
ENV PORT 8080
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV HTTPS_ENABLED false
|
ENV HTTPS_ENABLED false
|
||||||
|
@@ -3,13 +3,10 @@ ARG POSTHOG_API_KEY=posthog-api-key
|
|||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||||
|
|
||||||
FROM node:20-alpine AS base
|
FROM node:20-slim AS base
|
||||||
|
|
||||||
FROM base AS frontend-dependencies
|
FROM base AS frontend-dependencies
|
||||||
|
|
||||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY frontend/package.json frontend/package-lock.json ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
@@ -45,8 +42,8 @@ RUN npm run build
|
|||||||
FROM base AS frontend-runner
|
FROM base AS frontend-runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN groupadd --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 non-root-user
|
RUN useradd --system --uid 1001 --gid nodejs non-root-user
|
||||||
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
||||||
|
|
||||||
@@ -56,21 +53,23 @@ USER non-root-user
|
|||||||
## BACKEND
|
## BACKEND
|
||||||
##
|
##
|
||||||
FROM base AS backend-build
|
FROM base AS backend-build
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
|
||||||
&& adduser --system --uid 1001 non-root-user
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install all required dependencies for build
|
# Install all required dependencies for build
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev \
|
||||||
freetds-dev
|
freetds-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN groupadd --system --gid 1001 nodejs
|
||||||
|
RUN useradd --system --uid 1001 --gid nodejs non-root-user
|
||||||
|
|
||||||
COPY backend/package*.json ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
@@ -86,18 +85,19 @@ FROM base AS backend-runner
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install all required dependencies for runtime
|
# Install all required dependencies for runtime
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev \
|
||||||
freetds-dev
|
freetds-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Configure ODBC
|
# Configure ODBC
|
||||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
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 ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
@@ -109,34 +109,36 @@ 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 apt-get update && apt-get install -y \
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
ca-certificates \
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
bash \
|
||||||
&& apk add infisical=0.31.1 && apk add --no-cache git
|
curl \
|
||||||
|
git \
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
# Install all required runtime dependencies
|
|
||||||
RUN apk --update add \
|
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev \
|
||||||
freetds-dev \
|
freetds-dev \
|
||||||
bash \
|
wget \
|
||||||
curl \
|
openssh-client \
|
||||||
git \
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
openssh
|
|
||||||
|
# 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/*
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
# Configure ODBC in production
|
# 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
|
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
|
||||||
|
|
||||||
# Setup user permissions
|
# Setup user permissions
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN groupadd --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 non-root-user
|
&& useradd --system --uid 1001 --gid nodejs non-root-user
|
||||||
|
|
||||||
# Give non-root-user permission to update SSL certs
|
# Give non-root-user permission to update SSL certs
|
||||||
RUN chown -R non-root-user /etc/ssl/certs
|
RUN chown -R non-root-user /etc/ssl/certs
|
||||||
@@ -154,11 +156,11 @@ ENV INTERCOM_ID=$INTERCOM_ID
|
|||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
|
|
||||||
COPY --from=backend-runner /app /backend
|
COPY --from=backend-runner /app /backend
|
||||||
|
|
||||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||||
|
|
||||||
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
|
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
|
|
||||||
ENV PORT 8080
|
ENV PORT 8080
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
@@ -166,6 +168,7 @@ ENV HTTPS_ENABLED false
|
|||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV STANDALONE_BUILD true
|
ENV STANDALONE_BUILD true
|
||||||
ENV STANDALONE_MODE true
|
ENV STANDALONE_MODE true
|
||||||
|
|
||||||
WORKDIR /backend
|
WORKDIR /backend
|
||||||
|
|
||||||
ENV TELEMETRY_ENABLED true
|
ENV TELEMETRY_ENABLED true
|
||||||
|
@@ -1,23 +1,22 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM node:20-alpine AS build
|
FROM node:20-slim AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Required for pkcs11js
|
# Required for pkcs11js
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
openssh
|
openssh-client
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apt-get install -y \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
|
freetds-dev \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev
|
||||||
freetds-dev
|
|
||||||
|
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
@@ -26,36 +25,36 @@ COPY . .
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM node:20-alpine
|
FROM node:20-slim
|
||||||
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 \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++
|
g++
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apt-get install -y \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
|
freetds-dev \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev
|
||||||
freetds-dev
|
|
||||||
|
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
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 .
|
||||||
|
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
# Install Infisical CLI
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
RUN apt-get install -y curl bash && \
|
||||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||||
|
apt-get update && apt-get install -y infisical=0.8.1 git
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||||
CMD node healthcheck.js
|
CMD node healthcheck.js
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM node:20-slim
|
||||||
|
|
||||||
# ? Setup a test SoftHSM module. In production a real HSM is used.
|
# ? Setup a test SoftHSM module. In production a real HSM is used.
|
||||||
|
|
||||||
@@ -7,32 +7,32 @@ ARG SOFTHSM2_VERSION=2.5.0
|
|||||||
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
||||||
SOFTHSM2_SOURCES=/tmp/softhsm2
|
SOFTHSM2_SOURCES=/tmp/softhsm2
|
||||||
|
|
||||||
# install build dependencies including python3 (required for pkcs11js and partially TDS driver)
|
# Install build dependencies including python3 (required for pkcs11js and partially TDS driver)
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
alpine-sdk \
|
build-essential \
|
||||||
autoconf \
|
autoconf \
|
||||||
automake \
|
automake \
|
||||||
git \
|
git \
|
||||||
libtool \
|
libtool \
|
||||||
openssl-dev \
|
libssl-dev \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
openssh
|
openssh-client \
|
||||||
|
curl \
|
||||||
|
pkg-config
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apt-get install -y \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
freetds-dev \
|
||||||
freetds-dev
|
freetds-bin \
|
||||||
|
tdsodbc
|
||||||
|
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
# build and install SoftHSM2
|
|
||||||
|
|
||||||
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
||||||
WORKDIR ${SOFTHSM2_SOURCES}
|
WORKDIR ${SOFTHSM2_SOURCES}
|
||||||
|
|
||||||
@@ -45,16 +45,18 @@ RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \
|
|||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
RUN rm -fr ${SOFTHSM2_SOURCES}
|
RUN rm -fr ${SOFTHSM2_SOURCES}
|
||||||
|
|
||||||
# install pkcs11-tool
|
# Install pkcs11-tool
|
||||||
RUN apk --update add opensc
|
RUN apt-get install -y opensc
|
||||||
|
|
||||||
RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
RUN mkdir -p /etc/softhsm2/tokens && \
|
||||||
|
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
||||||
|
|
||||||
# ? App setup
|
# ? App setup
|
||||||
|
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
# Install Infisical CLI
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
apt-get update && \
|
||||||
|
apt-get install -y infisical=0.8.1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
@@ -535,6 +535,107 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }]
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each(secretTestCases)("Bulk upsert secrets in path $path", async ({ secret, path }) => {
|
||||||
|
const updateSharedSecRes = await testServer.inject({
|
||||||
|
method: "PATCH",
|
||||||
|
url: `/api/v3/secrets/batch/raw`,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${authToken}`
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
workspaceId: seedData1.projectV3.id,
|
||||||
|
environment: seedData1.environment.slug,
|
||||||
|
secretPath: path,
|
||||||
|
mode: "upsert",
|
||||||
|
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||||
|
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||||
|
secretValue: "update-value",
|
||||||
|
secretComment: secret.comment
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(updateSharedSecRes.statusCode).toBe(200);
|
||||||
|
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
|
||||||
|
expect(updateSharedSecPayload).toHaveProperty("secrets");
|
||||||
|
|
||||||
|
// bulk ones should exist
|
||||||
|
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||||
|
expect(secrets).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
Array.from(Array(5)).map((_e, i) =>
|
||||||
|
expect.objectContaining({
|
||||||
|
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||||
|
secretValue: "update-value",
|
||||||
|
type: SecretType.Shared
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Bulk upsert secrets in path multiple paths", async () => {
|
||||||
|
const firstBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
|
||||||
|
secretKey: `BULK-KEY-${secretTestCases[0].secret.key}-${i + 1}`,
|
||||||
|
secretValue: "update-value",
|
||||||
|
secretComment: "comment",
|
||||||
|
secretPath: secretTestCases[0].path
|
||||||
|
}));
|
||||||
|
const secondBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
|
||||||
|
secretKey: `BULK-KEY-${secretTestCases[1].secret.key}-${i + 1}`,
|
||||||
|
secretValue: "update-value",
|
||||||
|
secretComment: "comment",
|
||||||
|
secretPath: secretTestCases[1].path
|
||||||
|
}));
|
||||||
|
const testSecrets = [...firstBatchSecrets, ...secondBatchSecrets];
|
||||||
|
|
||||||
|
const updateSharedSecRes = await testServer.inject({
|
||||||
|
method: "PATCH",
|
||||||
|
url: `/api/v3/secrets/batch/raw`,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${authToken}`
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
workspaceId: seedData1.projectV3.id,
|
||||||
|
environment: seedData1.environment.slug,
|
||||||
|
mode: "upsert",
|
||||||
|
secrets: testSecrets
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(updateSharedSecRes.statusCode).toBe(200);
|
||||||
|
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
|
||||||
|
expect(updateSharedSecPayload).toHaveProperty("secrets");
|
||||||
|
|
||||||
|
// bulk ones should exist
|
||||||
|
const firstBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[0].path);
|
||||||
|
expect(firstBatchSecretsOnInfisical).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
firstBatchSecrets.map((el) =>
|
||||||
|
expect.objectContaining({
|
||||||
|
secretKey: el.secretKey,
|
||||||
|
secretValue: "update-value",
|
||||||
|
type: SecretType.Shared
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const secondBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[1].path);
|
||||||
|
expect(secondBatchSecretsOnInfisical).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
secondBatchSecrets.map((el) =>
|
||||||
|
expect.objectContaining({
|
||||||
|
secretKey: el.secretKey,
|
||||||
|
secretValue: "update-value",
|
||||||
|
type: SecretType.Shared
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(testSecrets.map((el) => deleteSecret({ path: el.secretPath, key: el.secretKey })));
|
||||||
|
});
|
||||||
|
|
||||||
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
|
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
|
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
|
||||||
|
2340
backend/package-lock.json
generated
2340
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -60,6 +60,13 @@
|
|||||||
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status",
|
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status",
|
||||||
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback",
|
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback",
|
||||||
"migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock",
|
"migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock",
|
||||||
|
"migration:up-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||||
|
"migration:down-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||||
|
"migration:list-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||||
|
"migration:latest-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||||
|
"migration:status-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||||
|
"migration:rollback-dev": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||||
|
"migration:unlock-dev": "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 ./dist/db/knexfile.ts --client pg seed:run",
|
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||||
@@ -129,7 +136,7 @@
|
|||||||
"@fastify/etag": "^5.1.0",
|
"@fastify/etag": "^5.1.0",
|
||||||
"@fastify/formbody": "^7.4.0",
|
"@fastify/formbody": "^7.4.0",
|
||||||
"@fastify/helmet": "^11.1.1",
|
"@fastify/helmet": "^11.1.1",
|
||||||
"@fastify/multipart": "8.3.0",
|
"@fastify/multipart": "8.3.1",
|
||||||
"@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/request-context": "^5.1.0",
|
||||||
@@ -138,6 +145,7 @@
|
|||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
"@google-cloud/kms": "^4.5.0",
|
"@google-cloud/kms": "^4.5.0",
|
||||||
|
"@infisical/quic": "^1.0.8",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/auth-app": "^7.1.1",
|
"@octokit/auth-app": "^7.1.1",
|
||||||
"@octokit/plugin-retry": "^5.0.5",
|
"@octokit/plugin-retry": "^5.0.5",
|
||||||
@@ -145,10 +153,10 @@
|
|||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
"@octopusdeploy/api-client": "^3.4.1",
|
"@octopusdeploy/api-client": "^3.4.1",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
|
|
||||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
|
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
|
||||||
"@opentelemetry/exporter-prometheus": "^0.55.0",
|
"@opentelemetry/exporter-prometheus": "^0.55.0",
|
||||||
"@opentelemetry/instrumentation": "^0.55.0",
|
"@opentelemetry/instrumentation": "^0.55.0",
|
||||||
|
"@opentelemetry/instrumentation-http": "^0.57.2",
|
||||||
"@opentelemetry/resources": "^1.28.0",
|
"@opentelemetry/resources": "^1.28.0",
|
||||||
"@opentelemetry/sdk-metrics": "^1.28.0",
|
"@opentelemetry/sdk-metrics": "^1.28.0",
|
||||||
"@opentelemetry/semantic-conventions": "^1.27.0",
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
||||||
@@ -156,8 +164,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",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
"@slack/oauth": "^3.0.1",
|
"@slack/oauth": "^3.0.2",
|
||||||
"@slack/web-api": "^7.3.4",
|
"@slack/web-api": "^7.8.0",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"argon2": "^0.31.2",
|
"argon2": "^0.31.2",
|
||||||
@@ -169,6 +177,7 @@
|
|||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
"connect-redis": "^7.1.1",
|
"connect-redis": "^7.1.1",
|
||||||
"cron": "^3.1.7",
|
"cron": "^3.1.7",
|
||||||
|
"dd-trace": "^5.40.0",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^4.28.1",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
@@ -177,6 +186,7 @@
|
|||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"hdb": "^0.19.10",
|
"hdb": "^0.19.10",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
|
"isomorphic-dompurify": "^2.22.0",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
@@ -189,7 +199,7 @@
|
|||||||
"mongodb": "^6.8.1",
|
"mongodb": "^6.8.1",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.8",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
"odbc": "^2.4.9",
|
"odbc": "^2.4.9",
|
||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
|
13
backend/src/@types/fastify.d.ts
vendored
13
backend/src/@types/fastify.d.ts
vendored
@@ -13,9 +13,13 @@ import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/
|
|||||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||||
|
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
|
||||||
|
import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
|
||||||
|
import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
|
||||||
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||||
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";
|
||||||
@@ -126,6 +130,11 @@ declare module "fastify" {
|
|||||||
isUserCompleted: string;
|
isUserCompleted: string;
|
||||||
providerAuthToken: string;
|
providerAuthToken: string;
|
||||||
};
|
};
|
||||||
|
kmipUser: {
|
||||||
|
projectId: string;
|
||||||
|
clientId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
||||||
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
|
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
|
||||||
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
|
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
|
||||||
@@ -218,11 +227,15 @@ declare module "fastify" {
|
|||||||
totp: TTotpServiceFactory;
|
totp: TTotpServiceFactory;
|
||||||
appConnection: TAppConnectionServiceFactory;
|
appConnection: TAppConnectionServiceFactory;
|
||||||
secretSync: TSecretSyncServiceFactory;
|
secretSync: TSecretSyncServiceFactory;
|
||||||
|
kmip: TKmipServiceFactory;
|
||||||
|
kmipOperation: TKmipOperationServiceFactory;
|
||||||
|
gateway: TGatewayServiceFactory;
|
||||||
};
|
};
|
||||||
// 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
|
||||||
store: {
|
store: {
|
||||||
user: Pick<TUserDALFactory, "findById">;
|
user: Pick<TUserDALFactory, "findById">;
|
||||||
|
kmipClient: Pick<TKmipClientDALFactory, "findByProjectAndClientId">;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
backend/src/@types/knex.d.ts
vendored
48
backend/src/@types/knex.d.ts
vendored
@@ -68,6 +68,9 @@ import {
|
|||||||
TExternalKms,
|
TExternalKms,
|
||||||
TExternalKmsInsert,
|
TExternalKmsInsert,
|
||||||
TExternalKmsUpdate,
|
TExternalKmsUpdate,
|
||||||
|
TGateways,
|
||||||
|
TGatewaysInsert,
|
||||||
|
TGatewaysUpdate,
|
||||||
TGitAppInstallSessions,
|
TGitAppInstallSessions,
|
||||||
TGitAppInstallSessionsInsert,
|
TGitAppInstallSessionsInsert,
|
||||||
TGitAppInstallSessionsUpdate,
|
TGitAppInstallSessionsUpdate,
|
||||||
@@ -143,6 +146,18 @@ import {
|
|||||||
TInternalKms,
|
TInternalKms,
|
||||||
TInternalKmsInsert,
|
TInternalKmsInsert,
|
||||||
TInternalKmsUpdate,
|
TInternalKmsUpdate,
|
||||||
|
TKmipClientCertificates,
|
||||||
|
TKmipClientCertificatesInsert,
|
||||||
|
TKmipClientCertificatesUpdate,
|
||||||
|
TKmipClients,
|
||||||
|
TKmipClientsInsert,
|
||||||
|
TKmipClientsUpdate,
|
||||||
|
TKmipOrgConfigs,
|
||||||
|
TKmipOrgConfigsInsert,
|
||||||
|
TKmipOrgConfigsUpdate,
|
||||||
|
TKmipOrgServerCertificates,
|
||||||
|
TKmipOrgServerCertificatesInsert,
|
||||||
|
TKmipOrgServerCertificatesUpdate,
|
||||||
TKmsKeys,
|
TKmsKeys,
|
||||||
TKmsKeysInsert,
|
TKmsKeysInsert,
|
||||||
TKmsKeysUpdate,
|
TKmsKeysUpdate,
|
||||||
@@ -167,6 +182,9 @@ import {
|
|||||||
TOrgBots,
|
TOrgBots,
|
||||||
TOrgBotsInsert,
|
TOrgBotsInsert,
|
||||||
TOrgBotsUpdate,
|
TOrgBotsUpdate,
|
||||||
|
TOrgGatewayConfig,
|
||||||
|
TOrgGatewayConfigInsert,
|
||||||
|
TOrgGatewayConfigUpdate,
|
||||||
TOrgMemberships,
|
TOrgMemberships,
|
||||||
TOrgMembershipsInsert,
|
TOrgMembershipsInsert,
|
||||||
TOrgMembershipsUpdate,
|
TOrgMembershipsUpdate,
|
||||||
@@ -188,6 +206,9 @@ import {
|
|||||||
TProjectEnvironments,
|
TProjectEnvironments,
|
||||||
TProjectEnvironmentsInsert,
|
TProjectEnvironmentsInsert,
|
||||||
TProjectEnvironmentsUpdate,
|
TProjectEnvironmentsUpdate,
|
||||||
|
TProjectGateways,
|
||||||
|
TProjectGatewaysInsert,
|
||||||
|
TProjectGatewaysUpdate,
|
||||||
TProjectKeys,
|
TProjectKeys,
|
||||||
TProjectKeysInsert,
|
TProjectKeysInsert,
|
||||||
TProjectKeysUpdate,
|
TProjectKeysUpdate,
|
||||||
@@ -902,5 +923,32 @@ declare module "knex/types/tables" {
|
|||||||
TAppConnectionsUpdate
|
TAppConnectionsUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
|
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
|
||||||
|
[TableName.KmipClient]: KnexOriginal.CompositeTableType<TKmipClients, TKmipClientsInsert, TKmipClientsUpdate>;
|
||||||
|
[TableName.KmipOrgConfig]: KnexOriginal.CompositeTableType<
|
||||||
|
TKmipOrgConfigs,
|
||||||
|
TKmipOrgConfigsInsert,
|
||||||
|
TKmipOrgConfigsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.KmipOrgServerCertificates]: KnexOriginal.CompositeTableType<
|
||||||
|
TKmipOrgServerCertificates,
|
||||||
|
TKmipOrgServerCertificatesInsert,
|
||||||
|
TKmipOrgServerCertificatesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType<
|
||||||
|
TKmipClientCertificates,
|
||||||
|
TKmipClientCertificatesInsert,
|
||||||
|
TKmipClientCertificatesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
|
||||||
|
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectGateways,
|
||||||
|
TProjectGatewaysInsert,
|
||||||
|
TProjectGatewaysUpdate
|
||||||
|
>;
|
||||||
|
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
|
||||||
|
TOrgGatewayConfig,
|
||||||
|
TOrgGatewayConfigInsert,
|
||||||
|
TOrgGatewayConfigUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -39,7 +39,7 @@ export default {
|
|||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "infisical_migrations",
|
tableName: "infisical_migrations",
|
||||||
loadExtensions: [".mjs"]
|
loadExtensions: [".mjs", ".ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
@@ -64,7 +64,7 @@ export default {
|
|||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "infisical_migrations",
|
tableName: "infisical_migrations",
|
||||||
loadExtensions: [".mjs"]
|
loadExtensions: [".mjs", ".ts"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as Knex.Config;
|
} as Knex.Config;
|
||||||
|
108
backend/src/db/migrations/20250203141127_add-kmip.ts
Normal file
108
backend/src/db/migrations/20250203141127_add-kmip.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
|
||||||
|
if (!hasKmipClientTable) {
|
||||||
|
await knex.schema.createTable(TableName.KmipClient, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.specificType("permissions", "text[]");
|
||||||
|
t.string("description");
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig);
|
||||||
|
if (!hasKmipOrgPkiConfig) {
|
||||||
|
await knex.schema.createTable(TableName.KmipOrgConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.unique("orgId");
|
||||||
|
|
||||||
|
t.string("caKeyAlgorithm").notNullable();
|
||||||
|
|
||||||
|
t.datetime("rootCaIssuedAt").notNullable();
|
||||||
|
t.datetime("rootCaExpiration").notNullable();
|
||||||
|
t.string("rootCaSerialNumber").notNullable();
|
||||||
|
t.binary("encryptedRootCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedRootCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.datetime("serverIntermediateCaIssuedAt").notNullable();
|
||||||
|
t.datetime("serverIntermediateCaExpiration").notNullable();
|
||||||
|
t.string("serverIntermediateCaSerialNumber");
|
||||||
|
t.binary("encryptedServerIntermediateCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedServerIntermediateCaChain").notNullable();
|
||||||
|
t.binary("encryptedServerIntermediateCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.datetime("clientIntermediateCaIssuedAt").notNullable();
|
||||||
|
t.datetime("clientIntermediateCaExpiration").notNullable();
|
||||||
|
t.string("clientIntermediateCaSerialNumber").notNullable();
|
||||||
|
t.binary("encryptedClientIntermediateCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedClientIntermediateCaChain").notNullable();
|
||||||
|
t.binary("encryptedClientIntermediateCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.KmipOrgConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates);
|
||||||
|
if (!hasKmipOrgServerCertTable) {
|
||||||
|
await knex.schema.createTable(TableName.KmipOrgServerCertificates, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.string("commonName").notNullable();
|
||||||
|
t.string("altNames").notNullable();
|
||||||
|
t.string("serialNumber").notNullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
t.datetime("issuedAt").notNullable();
|
||||||
|
t.datetime("expiration").notNullable();
|
||||||
|
t.binary("encryptedCertificate").notNullable();
|
||||||
|
t.binary("encryptedChain").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
|
||||||
|
if (!hasKmipClientCertTable) {
|
||||||
|
await knex.schema.createTable(TableName.KmipClientCertificates, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("kmipClientId").notNullable();
|
||||||
|
t.foreign("kmipClientId").references("id").inTable(TableName.KmipClient).onDelete("CASCADE");
|
||||||
|
t.string("serialNumber").notNullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
t.datetime("issuedAt").notNullable();
|
||||||
|
t.datetime("expiration").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig);
|
||||||
|
if (hasKmipOrgPkiConfig) {
|
||||||
|
await knex.schema.dropTable(TableName.KmipOrgConfig);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.KmipOrgConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates);
|
||||||
|
if (hasKmipOrgServerCertTable) {
|
||||||
|
await knex.schema.dropTable(TableName.KmipOrgServerCertificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
|
||||||
|
if (hasKmipClientCertTable) {
|
||||||
|
await knex.schema.dropTable(TableName.KmipClientCertificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
|
||||||
|
if (hasKmipClientTable) {
|
||||||
|
await knex.schema.dropTable(TableName.KmipClient);
|
||||||
|
}
|
||||||
|
}
|
115
backend/src/db/migrations/20250212191958_create-gateway.ts
Normal file
115
backend/src/db/migrations/20250212191958_create-gateway.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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.OrgGatewayConfig))) {
|
||||||
|
await knex.schema.createTable(TableName.OrgGatewayConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("rootCaKeyAlgorithm").notNullable();
|
||||||
|
|
||||||
|
t.datetime("rootCaIssuedAt").notNullable();
|
||||||
|
t.datetime("rootCaExpiration").notNullable();
|
||||||
|
t.string("rootCaSerialNumber").notNullable();
|
||||||
|
t.binary("encryptedRootCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedRootCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.datetime("clientCaIssuedAt").notNullable();
|
||||||
|
t.datetime("clientCaExpiration").notNullable();
|
||||||
|
t.string("clientCaSerialNumber");
|
||||||
|
t.binary("encryptedClientCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedClientCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.string("clientCertSerialNumber").notNullable();
|
||||||
|
t.string("clientCertKeyAlgorithm").notNullable();
|
||||||
|
t.datetime("clientCertIssuedAt").notNullable();
|
||||||
|
t.datetime("clientCertExpiration").notNullable();
|
||||||
|
t.binary("encryptedClientCertificate").notNullable();
|
||||||
|
t.binary("encryptedClientPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.datetime("gatewayCaIssuedAt").notNullable();
|
||||||
|
t.datetime("gatewayCaExpiration").notNullable();
|
||||||
|
t.string("gatewayCaSerialNumber").notNullable();
|
||||||
|
t.binary("encryptedGatewayCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedGatewayCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.unique("orgId");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Gateway))) {
|
||||||
|
await knex.schema.createTable(TableName.Gateway, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.string("serialNumber").notNullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
t.datetime("issuedAt").notNullable();
|
||||||
|
t.datetime("expiration").notNullable();
|
||||||
|
t.datetime("heartbeat");
|
||||||
|
|
||||||
|
t.binary("relayAddress").notNullable();
|
||||||
|
|
||||||
|
t.uuid("orgGatewayRootCaId").notNullable();
|
||||||
|
t.foreign("orgGatewayRootCaId").references("id").inTable(TableName.OrgGatewayConfig).onDelete("CASCADE");
|
||||||
|
|
||||||
|
t.uuid("identityId").notNullable();
|
||||||
|
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Gateway);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ProjectGateway))) {
|
||||||
|
await knex.schema.createTable(TableName.ProjectGateway, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
|
||||||
|
t.uuid("gatewayId").notNullable();
|
||||||
|
t.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("CASCADE");
|
||||||
|
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||||
|
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||||
|
// not setting a foreign constraint so that cascade effects are not triggered
|
||||||
|
if (!doesGatewayColExist) {
|
||||||
|
t.uuid("projectGatewayId");
|
||||||
|
t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||||
|
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||||
|
if (doesGatewayColExist) t.dropColumn("projectGatewayId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ProjectGateway);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Gateway);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Gateway);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||||
|
}
|
25
backend/src/db/migrations/20250226021631_secret-requests.ts
Normal file
25
backend/src/db/migrations/20250226021631_secret-requests.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
|
||||||
|
if (!hasSharingTypeColumn) {
|
||||||
|
table.string("type", 32).defaultTo(SecretSharingType.Share).notNullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
|
||||||
|
if (hasSharingTypeColumn) {
|
||||||
|
table.dropColumn("type");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||||
|
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||||
|
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
if (!hasAuthConsentContentCol) {
|
||||||
|
t.text("authConsentContent");
|
||||||
|
}
|
||||||
|
if (!hasPageFrameContentCol) {
|
||||||
|
t.text("pageFrameContent");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||||
|
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
if (hasAuthConsentContentCol) {
|
||||||
|
t.dropColumn("authConsentContent");
|
||||||
|
}
|
||||||
|
if (hasPageFrameContentCol) {
|
||||||
|
t.dropColumn("pageFrameContent");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
for await (const tableName of [
|
||||||
|
TableName.SecretV2,
|
||||||
|
TableName.SecretVersionV2,
|
||||||
|
TableName.SecretApprovalRequestSecretV2
|
||||||
|
]) {
|
||||||
|
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||||
|
|
||||||
|
if (hasReminderNoteCol) {
|
||||||
|
await knex.schema.alterTable(tableName, (t) => {
|
||||||
|
t.string("reminderNote", 1024).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
for await (const tableName of [
|
||||||
|
TableName.SecretV2,
|
||||||
|
TableName.SecretVersionV2,
|
||||||
|
TableName.SecretApprovalRequestSecretV2
|
||||||
|
]) {
|
||||||
|
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||||
|
|
||||||
|
if (hasReminderNoteCol) {
|
||||||
|
await knex.schema.alterTable(tableName, (t) => {
|
||||||
|
t.string("reminderNote").alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
|
||||||
|
|
||||||
|
if (!hasProjectDescription) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||||
|
t.string("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
|
||||||
|
|
||||||
|
if (hasProjectDescription) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||||
|
t.dropColumn("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||||
|
t.string("comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment")) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||||
|
t.dropColumn("comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,45 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||||
|
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||||
|
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||||
|
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||||
|
if (!hasSecretVersionV2UserActorId) {
|
||||||
|
t.uuid("userActorId");
|
||||||
|
t.foreign("userActorId").references("id").inTable(TableName.Users);
|
||||||
|
}
|
||||||
|
if (!hasSecretVersionV2IdentityActorId) {
|
||||||
|
t.uuid("identityActorId");
|
||||||
|
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
|
||||||
|
}
|
||||||
|
if (!hasSecretVersionV2ActorType) {
|
||||||
|
t.string("actorType");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||||
|
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||||
|
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||||
|
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||||
|
if (hasSecretVersionV2UserActorId) {
|
||||||
|
t.dropColumn("userActorId");
|
||||||
|
}
|
||||||
|
if (hasSecretVersionV2IdentityActorId) {
|
||||||
|
t.dropColumn("identityActorId");
|
||||||
|
}
|
||||||
|
if (hasSecretVersionV2ActorType) {
|
||||||
|
t.dropColumn("actorType");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -42,7 +42,7 @@ export const getMigrationEnvConfig = () => {
|
|||||||
console.error("Invalid environment variables. Check the error below");
|
console.error("Invalid environment variables. Check the error below");
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(
|
console.error(
|
||||||
"Migration is now automatic at startup. Please remove this step from your workflow and start the application as normal."
|
"Infisical now automatically runs database migrations during boot up, so you no longer need to run them separately."
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(parsedEnv.error.issues);
|
console.error(parsedEnv.error.issues);
|
||||||
|
@@ -26,7 +26,8 @@ export const DynamicSecretsSchema = z.object({
|
|||||||
statusDetails: z.string().nullable().optional(),
|
statusDetails: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
encryptedInput: zodBuffer
|
encryptedInput: zodBuffer,
|
||||||
|
projectGatewayId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||||
|
29
backend/src/db/schemas/gateways.ts
Normal file
29
backend/src/db/schemas/gateways.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const GatewaysSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
serialNumber: z.string(),
|
||||||
|
keyAlgorithm: z.string(),
|
||||||
|
issuedAt: z.date(),
|
||||||
|
expiration: z.date(),
|
||||||
|
heartbeat: z.date().nullable().optional(),
|
||||||
|
relayAddress: zodBuffer,
|
||||||
|
orgGatewayRootCaId: z.string().uuid(),
|
||||||
|
identityId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TGateways = z.infer<typeof GatewaysSchema>;
|
||||||
|
export type TGatewaysInsert = Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>;
|
||||||
|
export type TGatewaysUpdate = Partial<Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>>;
|
@@ -20,6 +20,7 @@ export * from "./certificates";
|
|||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
export * from "./external-kms";
|
export * from "./external-kms";
|
||||||
|
export * from "./gateways";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
export * from "./git-app-org";
|
export * from "./git-app-org";
|
||||||
export * from "./group-project-membership-roles";
|
export * from "./group-project-membership-roles";
|
||||||
@@ -45,6 +46,10 @@ export * from "./incident-contacts";
|
|||||||
export * from "./integration-auths";
|
export * from "./integration-auths";
|
||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
export * from "./internal-kms";
|
export * from "./internal-kms";
|
||||||
|
export * from "./kmip-client-certificates";
|
||||||
|
export * from "./kmip-clients";
|
||||||
|
export * from "./kmip-org-configs";
|
||||||
|
export * from "./kmip-org-server-certificates";
|
||||||
export * from "./kms-key-versions";
|
export * from "./kms-key-versions";
|
||||||
export * from "./kms-keys";
|
export * from "./kms-keys";
|
||||||
export * from "./kms-root-config";
|
export * from "./kms-root-config";
|
||||||
@@ -53,6 +58,7 @@ export * from "./ldap-group-maps";
|
|||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./oidc-configs";
|
export * from "./oidc-configs";
|
||||||
export * from "./org-bots";
|
export * from "./org-bots";
|
||||||
|
export * from "./org-gateway-config";
|
||||||
export * from "./org-memberships";
|
export * from "./org-memberships";
|
||||||
export * from "./org-roles";
|
export * from "./org-roles";
|
||||||
export * from "./organizations";
|
export * from "./organizations";
|
||||||
@@ -61,6 +67,7 @@ export * from "./pki-collection-items";
|
|||||||
export * from "./pki-collections";
|
export * from "./pki-collections";
|
||||||
export * from "./project-bots";
|
export * from "./project-bots";
|
||||||
export * from "./project-environments";
|
export * from "./project-environments";
|
||||||
|
export * from "./project-gateways";
|
||||||
export * from "./project-keys";
|
export * from "./project-keys";
|
||||||
export * from "./project-memberships";
|
export * from "./project-memberships";
|
||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
|
23
backend/src/db/schemas/kmip-client-certificates.ts
Normal file
23
backend/src/db/schemas/kmip-client-certificates.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 KmipClientCertificatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
kmipClientId: z.string().uuid(),
|
||||||
|
serialNumber: z.string(),
|
||||||
|
keyAlgorithm: z.string(),
|
||||||
|
issuedAt: z.date(),
|
||||||
|
expiration: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TKmipClientCertificates = z.infer<typeof KmipClientCertificatesSchema>;
|
||||||
|
export type TKmipClientCertificatesInsert = Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TKmipClientCertificatesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
20
backend/src/db/schemas/kmip-clients.ts
Normal file
20
backend/src/db/schemas/kmip-clients.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const KmipClientsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
permissions: z.string().array().nullable().optional(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
projectId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TKmipClients = z.infer<typeof KmipClientsSchema>;
|
||||||
|
export type TKmipClientsInsert = Omit<z.input<typeof KmipClientsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TKmipClientsUpdate = Partial<Omit<z.input<typeof KmipClientsSchema>, TImmutableDBKeys>>;
|
39
backend/src/db/schemas/kmip-org-configs.ts
Normal file
39
backend/src/db/schemas/kmip-org-configs.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const KmipOrgConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
caKeyAlgorithm: z.string(),
|
||||||
|
rootCaIssuedAt: z.date(),
|
||||||
|
rootCaExpiration: z.date(),
|
||||||
|
rootCaSerialNumber: z.string(),
|
||||||
|
encryptedRootCaCertificate: zodBuffer,
|
||||||
|
encryptedRootCaPrivateKey: zodBuffer,
|
||||||
|
serverIntermediateCaIssuedAt: z.date(),
|
||||||
|
serverIntermediateCaExpiration: z.date(),
|
||||||
|
serverIntermediateCaSerialNumber: z.string().nullable().optional(),
|
||||||
|
encryptedServerIntermediateCaCertificate: zodBuffer,
|
||||||
|
encryptedServerIntermediateCaChain: zodBuffer,
|
||||||
|
encryptedServerIntermediateCaPrivateKey: zodBuffer,
|
||||||
|
clientIntermediateCaIssuedAt: z.date(),
|
||||||
|
clientIntermediateCaExpiration: z.date(),
|
||||||
|
clientIntermediateCaSerialNumber: z.string(),
|
||||||
|
encryptedClientIntermediateCaCertificate: zodBuffer,
|
||||||
|
encryptedClientIntermediateCaChain: zodBuffer,
|
||||||
|
encryptedClientIntermediateCaPrivateKey: zodBuffer,
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TKmipOrgConfigs = z.infer<typeof KmipOrgConfigsSchema>;
|
||||||
|
export type TKmipOrgConfigsInsert = Omit<z.input<typeof KmipOrgConfigsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TKmipOrgConfigsUpdate = Partial<Omit<z.input<typeof KmipOrgConfigsSchema>, TImmutableDBKeys>>;
|
29
backend/src/db/schemas/kmip-org-server-certificates.ts
Normal file
29
backend/src/db/schemas/kmip-org-server-certificates.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const KmipOrgServerCertificatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
commonName: z.string(),
|
||||||
|
altNames: z.string(),
|
||||||
|
serialNumber: z.string(),
|
||||||
|
keyAlgorithm: z.string(),
|
||||||
|
issuedAt: z.date(),
|
||||||
|
expiration: z.date(),
|
||||||
|
encryptedCertificate: zodBuffer,
|
||||||
|
encryptedChain: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TKmipOrgServerCertificates = z.infer<typeof KmipOrgServerCertificatesSchema>;
|
||||||
|
export type TKmipOrgServerCertificatesInsert = Omit<z.input<typeof KmipOrgServerCertificatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TKmipOrgServerCertificatesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof KmipOrgServerCertificatesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
@@ -113,6 +113,10 @@ export enum TableName {
|
|||||||
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||||
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
||||||
|
// Gateway
|
||||||
|
OrgGatewayConfig = "org_gateway_config",
|
||||||
|
Gateway = "gateways",
|
||||||
|
ProjectGateway = "project_gateways",
|
||||||
// junction tables with tags
|
// junction tables with tags
|
||||||
SecretV2JnTag = "secret_v2_tag_junction",
|
SecretV2JnTag = "secret_v2_tag_junction",
|
||||||
JnSecretTag = "secret_tag_junction",
|
JnSecretTag = "secret_tag_junction",
|
||||||
@@ -132,7 +136,11 @@ export enum TableName {
|
|||||||
SlackIntegrations = "slack_integrations",
|
SlackIntegrations = "slack_integrations",
|
||||||
ProjectSlackConfigs = "project_slack_configs",
|
ProjectSlackConfigs = "project_slack_configs",
|
||||||
AppConnection = "app_connections",
|
AppConnection = "app_connections",
|
||||||
SecretSync = "secret_syncs"
|
SecretSync = "secret_syncs",
|
||||||
|
KmipClient = "kmip_clients",
|
||||||
|
KmipOrgConfig = "kmip_org_configs",
|
||||||
|
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||||
|
KmipClientCertificates = "kmip_client_certificates"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
43
backend/src/db/schemas/org-gateway-config.ts
Normal file
43
backend/src/db/schemas/org-gateway-config.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// 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 OrgGatewayConfigSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
rootCaKeyAlgorithm: z.string(),
|
||||||
|
rootCaIssuedAt: z.date(),
|
||||||
|
rootCaExpiration: z.date(),
|
||||||
|
rootCaSerialNumber: z.string(),
|
||||||
|
encryptedRootCaCertificate: zodBuffer,
|
||||||
|
encryptedRootCaPrivateKey: zodBuffer,
|
||||||
|
clientCaIssuedAt: z.date(),
|
||||||
|
clientCaExpiration: z.date(),
|
||||||
|
clientCaSerialNumber: z.string().nullable().optional(),
|
||||||
|
encryptedClientCaCertificate: zodBuffer,
|
||||||
|
encryptedClientCaPrivateKey: zodBuffer,
|
||||||
|
clientCertSerialNumber: z.string(),
|
||||||
|
clientCertKeyAlgorithm: z.string(),
|
||||||
|
clientCertIssuedAt: z.date(),
|
||||||
|
clientCertExpiration: z.date(),
|
||||||
|
encryptedClientCertificate: zodBuffer,
|
||||||
|
encryptedClientPrivateKey: zodBuffer,
|
||||||
|
gatewayCaIssuedAt: z.date(),
|
||||||
|
gatewayCaExpiration: z.date(),
|
||||||
|
gatewayCaSerialNumber: z.string(),
|
||||||
|
encryptedGatewayCaCertificate: zodBuffer,
|
||||||
|
encryptedGatewayCaPrivateKey: zodBuffer,
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TOrgGatewayConfig = z.infer<typeof OrgGatewayConfigSchema>;
|
||||||
|
export type TOrgGatewayConfigInsert = Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>;
|
||||||
|
export type TOrgGatewayConfigUpdate = Partial<Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>>;
|
20
backend/src/db/schemas/project-gateways.ts
Normal file
20
backend/src/db/schemas/project-gateways.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const ProjectGatewaysSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
projectId: z.string(),
|
||||||
|
gatewayId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectGateways = z.infer<typeof ProjectGatewaysSchema>;
|
||||||
|
export type TProjectGatewaysInsert = Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>;
|
||||||
|
export type TProjectGatewaysUpdate = Partial<Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>>;
|
@@ -13,7 +13,8 @@ export const SecretApprovalRequestsReviewersSchema = z.object({
|
|||||||
requestId: z.string().uuid(),
|
requestId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
reviewerUserId: z.string().uuid()
|
reviewerUserId: z.string().uuid(),
|
||||||
|
comment: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
||||||
|
@@ -15,7 +15,8 @@ export const SecretFoldersSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
envId: z.string().uuid(),
|
envId: z.string().uuid(),
|
||||||
parentId: z.string().uuid().nullable().optional(),
|
parentId: z.string().uuid().nullable().optional(),
|
||||||
isReserved: z.boolean().default(false).nullable().optional()
|
isReserved: z.boolean().default(false).nullable().optional(),
|
||||||
|
description: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
||||||
|
@@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const SecretSharingSchema = z.object({
|
export const SecretSharingSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedValue: z.string().nullable().optional(),
|
encryptedValue: z.string().nullable().optional(),
|
||||||
|
type: z.string(),
|
||||||
iv: z.string().nullable().optional(),
|
iv: z.string().nullable().optional(),
|
||||||
tag: z.string().nullable().optional(),
|
tag: z.string().nullable().optional(),
|
||||||
hashedHex: z.string().nullable().optional(),
|
hashedHex: z.string().nullable().optional(),
|
||||||
|
@@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
|
|||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
userActorId: z.string().uuid().nullable().optional(),
|
||||||
|
identityActorId: z.string().uuid().nullable().optional(),
|
||||||
|
actorType: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
||||||
|
@@ -23,7 +23,9 @@ export const SuperAdminSchema = z.object({
|
|||||||
defaultAuthOrgId: z.string().uuid().nullable().optional(),
|
defaultAuthOrgId: z.string().uuid().nullable().optional(),
|
||||||
enabledLoginMethods: z.string().array().nullable().optional(),
|
enabledLoginMethods: z.string().array().nullable().optional(),
|
||||||
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
||||||
encryptedSlackClientSecret: zodBuffer.nullable().optional()
|
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||||
|
authConsentContent: z.string().nullable().optional(),
|
||||||
|
pageFrameContent: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||||
|
265
backend/src/ee/routes/v1/gateway-router.ts
Normal file
265
backend/src/ee/routes/v1/gateway-router.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GatewaysSchema } from "@app/db/schemas";
|
||||||
|
import { isValidIp } from "@app/lib/ip";
|
||||||
|
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 { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const SanitizedGatewaySchema = GatewaysSchema.pick({
|
||||||
|
id: true,
|
||||||
|
identityId: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
issuedAt: true,
|
||||||
|
serialNumber: true,
|
||||||
|
heartbeat: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValidRelayAddress = (relayAddress: string) => {
|
||||||
|
const [ip, port] = relayAddress.split(":");
|
||||||
|
return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/register-identity",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
turnServerUsername: z.string(),
|
||||||
|
turnServerPassword: z.string(),
|
||||||
|
turnServerRealm: z.string(),
|
||||||
|
turnServerAddress: z.string(),
|
||||||
|
infisicalStaticIp: z.string().optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const relayDetails = await server.services.gateway.getGatewayRelayDetails(
|
||||||
|
req.permission.id,
|
||||||
|
req.permission.orgId,
|
||||||
|
req.permission.authMethod
|
||||||
|
);
|
||||||
|
return relayDetails;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/exchange-cert",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
relayAddress: z.string().refine(isValidRelayAddress, { message: "Invalid relay address" })
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string(),
|
||||||
|
privateKey: z.string(),
|
||||||
|
certificate: z.string(),
|
||||||
|
certificateChain: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({
|
||||||
|
identityOrg: req.permission.orgId,
|
||||||
|
identityId: req.permission.id,
|
||||||
|
relayAddress: req.body.relayAddress,
|
||||||
|
identityOrgAuthMethod: req.permission.authMethod
|
||||||
|
});
|
||||||
|
return gatewayCertificates;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/heartbeat",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.gateway.heartbeat({
|
||||||
|
orgPermission: req.permission
|
||||||
|
});
|
||||||
|
return { message: "Successfully registered heartbeat" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateways: SanitizedGatewaySchema.extend({
|
||||||
|
identity: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
projects: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateways = await server.services.gateway.listGateways({
|
||||||
|
orgPermission: req.permission
|
||||||
|
});
|
||||||
|
return { gateways };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/projects/:projectId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateways: SanitizedGatewaySchema.extend({
|
||||||
|
identity: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
projectGatewayId: z.string()
|
||||||
|
}).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateways = await server.services.gateway.getProjectGateways({
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
projectPermission: req.permission
|
||||||
|
});
|
||||||
|
return { gateways };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateway: SanitizedGatewaySchema.extend({
|
||||||
|
identity: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateway = await server.services.gateway.getGatewayById({
|
||||||
|
orgPermission: req.permission,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
return { gateway };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: slugSchema({ field: "name" }).optional(),
|
||||||
|
projectIds: z.string().array().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateway: SanitizedGatewaySchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateway = await server.services.gateway.updateGatewayById({
|
||||||
|
orgPermission: req.permission,
|
||||||
|
id: req.params.id,
|
||||||
|
name: req.body.name,
|
||||||
|
projectIds: req.body.projectIds
|
||||||
|
});
|
||||||
|
return { gateway };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateway: SanitizedGatewaySchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateway = await server.services.gateway.deleteGatewayById({
|
||||||
|
orgPermission: req.permission,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
return { gateway };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -7,8 +7,11 @@ import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
|||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||||
|
import { registerGatewayRouter } from "./gateway-router";
|
||||||
import { registerGroupRouter } from "./group-router";
|
import { registerGroupRouter } from "./group-router";
|
||||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||||
|
import { registerKmipRouter } from "./kmip-router";
|
||||||
|
import { registerKmipSpecRouter } from "./kmip-spec-router";
|
||||||
import { registerLdapRouter } from "./ldap-router";
|
import { registerLdapRouter } from "./ldap-router";
|
||||||
import { registerLicenseRouter } from "./license-router";
|
import { registerLicenseRouter } from "./license-router";
|
||||||
import { registerOidcRouter } from "./oidc-router";
|
import { registerOidcRouter } from "./oidc-router";
|
||||||
@@ -65,6 +68,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/dynamic-secrets" }
|
{ prefix: "/dynamic-secrets" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (pkiRouter) => {
|
async (pkiRouter) => {
|
||||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
||||||
@@ -110,4 +115,12 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (kmipRouter) => {
|
||||||
|
await kmipRouter.register(registerKmipRouter);
|
||||||
|
await kmipRouter.register(registerKmipSpecRouter, { prefix: "/spec" });
|
||||||
|
},
|
||||||
|
{ prefix: "/kmip" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
428
backend/src/ee/routes/v1/kmip-router.ts
Normal file
428
backend/src/ee/routes/v1/kmip-router.ts
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { KmipClientsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
|
||||||
|
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
|
|
||||||
|
const KmipClientResponseSchema = KmipClientsSchema.pick({
|
||||||
|
projectId: true,
|
||||||
|
name: true,
|
||||||
|
id: true,
|
||||||
|
description: true,
|
||||||
|
permissions: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerKmipRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/clients",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
permissions: z.nativeEnum(KmipPermission).array()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: KmipClientResponseSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const kmipClient = await server.services.kmip.createKmipClient({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: kmipClient.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
id: kmipClient.id,
|
||||||
|
name: kmipClient.name,
|
||||||
|
permissions: (kmipClient.permissions ?? []) as KmipPermission[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmipClient;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/clients/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
permissions: z.nativeEnum(KmipPermission).array()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: KmipClientResponseSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const kmipClient = await server.services.kmip.updateKmipClient({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.params,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: kmipClient.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
id: kmipClient.id,
|
||||||
|
name: kmipClient.name,
|
||||||
|
permissions: (kmipClient.permissions ?? []) as KmipPermission[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmipClient;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/clients/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: KmipClientResponseSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const kmipClient = await server.services.kmip.deleteKmipClient({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.params
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: kmipClient.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
id: kmipClient.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmipClient;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/clients/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: KmipClientResponseSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const kmipClient = await server.services.kmip.getKmipClient({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.params
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: kmipClient.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
id: kmipClient.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmipClient;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/clients",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List KMIP clients",
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
offset: z.coerce.number().min(0).optional().default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).optional().default(100),
|
||||||
|
orderBy: z.nativeEnum(KmipClientOrderBy).optional().default(KmipClientOrderBy.Name),
|
||||||
|
orderDirection: z.nativeEnum(OrderByDirection).optional().default(OrderByDirection.ASC),
|
||||||
|
search: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
kmipClients: KmipClientResponseSchema.array(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_KMIP_CLIENTS,
|
||||||
|
metadata: {
|
||||||
|
ids: kmipClients.map((key) => key.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { kmipClients, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/clients/:id/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
|
||||||
|
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string(),
|
||||||
|
certificateChain: z.string(),
|
||||||
|
certificate: z.string(),
|
||||||
|
privateKey: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const certificate = await server.services.kmip.createKmipClientCertificate({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
clientId: req.params.id,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.params.id,
|
||||||
|
serialNumber: certificate.serialNumber,
|
||||||
|
ttl: req.body.ttl,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serverCertificateChain: z.string(),
|
||||||
|
clientCertificateChain: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const chains = await server.services.kmip.setupOrgKmip({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SETUP_KMIP,
|
||||||
|
metadata: {
|
||||||
|
keyAlgorithm: req.body.caKeyAlgorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return chains;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serverCertificateChain: z.string(),
|
||||||
|
clientCertificateChain: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const kmip = await server.services.kmip.getOrgKmip({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_KMIP,
|
||||||
|
metadata: {
|
||||||
|
id: kmip.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmip;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/server-registration",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
hostnamesOrIps: validateAltNamesField,
|
||||||
|
commonName: z.string().trim().min(1).optional(),
|
||||||
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional().default(CertKeyAlgorithm.RSA_2048),
|
||||||
|
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
clientCertificateChain: z.string(),
|
||||||
|
certificateChain: z.string(),
|
||||||
|
certificate: z.string(),
|
||||||
|
privateKey: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const configs = await server.services.kmip.registerServer({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.REGISTER_KMIP_SERVER,
|
||||||
|
metadata: {
|
||||||
|
serverCertificateSerialNumber: configs.serverCertificateSerialNumber,
|
||||||
|
hostnamesOrIps: req.body.hostnamesOrIps,
|
||||||
|
commonName: req.body.commonName ?? "kmip-server",
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm,
|
||||||
|
ttl: req.body.ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
477
backend/src/ee/routes/v1/kmip-spec-router.ts
Normal file
477
backend/src/ee/routes/v1/kmip-spec-router.ts
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { KmsKeysSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.decorateRequest("kmipUser", null);
|
||||||
|
|
||||||
|
server.addHook("onRequest", async (req) => {
|
||||||
|
const clientId = req.headers["x-kmip-client-id"] as string;
|
||||||
|
const projectId = req.headers["x-kmip-project-id"] as string;
|
||||||
|
const clientCertSerialNumber = req.headers["x-kmip-client-certificate-serial-number"] as string;
|
||||||
|
const serverCertSerialNumber = req.headers["x-kmip-server-certificate-serial-number"] as string;
|
||||||
|
|
||||||
|
if (!serverCertSerialNumber) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Missing server certificate serial number from request"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientCertSerialNumber) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Missing client certificate serial number from request"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Missing client ID from request"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Missing project ID from request"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: assert that server certificate used is not revoked
|
||||||
|
// TODO: assert that client certificate used is not revoked
|
||||||
|
|
||||||
|
const kmipClient = await server.store.kmipClient.findByProjectAndClientId(projectId, clientId);
|
||||||
|
|
||||||
|
if (!kmipClient) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "KMIP client cannot be found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kmipClient.orgId !== req.permission.orgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client specified in the request does not belong in the organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.kmipUser = {
|
||||||
|
projectId,
|
||||||
|
clientId,
|
||||||
|
name: kmipClient.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/create",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "KMIP endpoint for creating managed objects",
|
||||||
|
body: z.object({
|
||||||
|
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: KmsKeysSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const object = await server.services.kmipOperation.create({
|
||||||
|
...req.kmipUser,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
algorithm: req.body.algorithm
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.kmipUser.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.kmipUser.clientId,
|
||||||
|
name: req.kmipUser.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.KMIP_OPERATION_CREATE,
|
||||||
|
metadata: {
|
||||||
|
id: object.id,
|
||||||
|
algorithm: req.body.algorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/get",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "KMIP endpoint for getting managed objects",
|
||||||
|
body: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
algorithm: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const object = await server.services.kmipOperation.get({
|
||||||
|
...req.kmipUser,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.body.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.kmipUser.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.kmipUser.clientId,
|
||||||
|
name: req.kmipUser.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.KMIP_OPERATION_GET,
|
||||||
|
metadata: {
|
||||||
|
id: object.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/get-attributes",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "KMIP endpoint for getting attributes of managed object",
|
||||||
|
body: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
algorithm: z.string(),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const object = await server.services.kmipOperation.getAttributes({
|
||||||
|
...req.kmipUser,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.body.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.kmipUser.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.kmipUser.clientId,
|
||||||
|
name: req.kmipUser.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.KMIP_OPERATION_GET_ATTRIBUTES,
|
||||||
|
metadata: {
|
||||||
|
id: object.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/destroy",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "KMIP endpoint for destroying managed objects",
|
||||||
|
body: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const object = await server.services.kmipOperation.destroy({
|
||||||
|
...req.kmipUser,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.body.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.kmipUser.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.kmipUser.clientId,
|
||||||
|
name: req.kmipUser.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.KMIP_OPERATION_DESTROY,
|
||||||
|
metadata: {
|
||||||
|
id: object.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/activate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "KMIP endpoint for activating managed object",
|
||||||
|
body: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
isActive: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const object = await server.services.kmipOperation.activate({
|
||||||
|
...req.kmipUser,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.body.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.kmipUser.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.kmipUser.clientId,
|
||||||
|
name: req.kmipUser.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.KMIP_OPERATION_ACTIVATE,
|
||||||
|
metadata: {
|
||||||
|
id: object.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/revoke",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "KMIP endpoint for revoking managed object",
|
||||||
|
body: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const object = await server.services.kmipOperation.revoke({
|
||||||
|
...req.kmipUser,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.body.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.kmipUser.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.kmipUser.clientId,
|
||||||
|
name: req.kmipUser.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.KMIP_OPERATION_REVOKE,
|
||||||
|
metadata: {
|
||||||
|
id: object.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/locate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "KMIP endpoint for locating managed objects",
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
objects: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
algorithm: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const objects = await server.services.kmipOperation.locate({
|
||||||
|
...req.kmipUser,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.kmipUser.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.kmipUser.clientId,
|
||||||
|
name: req.kmipUser.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.KMIP_OPERATION_LOCATE,
|
||||||
|
metadata: {
|
||||||
|
ids: objects.map((obj) => obj.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
objects
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/register",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "KMIP endpoint for registering managed object",
|
||||||
|
body: z.object({
|
||||||
|
key: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const object = await server.services.kmipOperation.register({
|
||||||
|
...req.kmipUser,
|
||||||
|
...req.body,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.kmipUser.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.KMIP_CLIENT,
|
||||||
|
metadata: {
|
||||||
|
clientId: req.kmipUser.clientId,
|
||||||
|
name: req.kmipUser.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.KMIP_OPERATION_REGISTER,
|
||||||
|
metadata: {
|
||||||
|
id: object.id,
|
||||||
|
algorithm: req.body.algorithm,
|
||||||
|
name: object.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -159,7 +159,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
id: z.string()
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]),
|
||||||
|
comment: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -175,8 +176,25 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
approvalId: req.params.id,
|
approvalId: req.params.id,
|
||||||
status: req.body.status
|
status: req.body.status,
|
||||||
|
comment: req.body.comment
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: review.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_REVIEW,
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: review.requestId,
|
||||||
|
reviewedBy: review.reviewerUserId,
|
||||||
|
status: review.status as ApprovalStatus,
|
||||||
|
comment: review.comment || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { review };
|
return { review };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -235,7 +253,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
const tagSchema = SecretTagsSchema.pick({
|
const tagSchema = SecretTagsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
name: true,
|
|
||||||
color: true
|
color: true
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
@@ -268,7 +285,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
statusChangedByUser: approvalRequestUser.optional(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
committerUser: approvalRequestUser,
|
committerUser: approvalRequestUser,
|
||||||
reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
|
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
commits: secretRawSchema
|
commits: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
||||||
|
@@ -35,7 +35,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
tags: SecretTagsSchema.pick({
|
tags: SecretTagsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
name: true,
|
|
||||||
color: true
|
color: true
|
||||||
}).array()
|
}).array()
|
||||||
})
|
})
|
||||||
|
@@ -21,6 +21,9 @@ import {
|
|||||||
TUpdateSecretSyncDTO
|
TUpdateSecretSyncDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { KmipPermission } from "../kmip/kmip-enum";
|
||||||
|
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
filter: {
|
filter: {
|
||||||
userAgentType?: UserAgentType;
|
userAgentType?: UserAgentType;
|
||||||
@@ -39,7 +42,14 @@ export type TListProjectAuditLogDTO = {
|
|||||||
|
|
||||||
export type TCreateAuditLogDTO = {
|
export type TCreateAuditLogDTO = {
|
||||||
event: Event;
|
event: Event;
|
||||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor;
|
actor:
|
||||||
|
| UserActor
|
||||||
|
| IdentityActor
|
||||||
|
| ServiceActor
|
||||||
|
| ScimClientActor
|
||||||
|
| PlatformActor
|
||||||
|
| UnknownUserActor
|
||||||
|
| KmipClientActor;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
} & BaseAuthData;
|
} & BaseAuthData;
|
||||||
@@ -156,6 +166,7 @@ export enum EventType {
|
|||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
||||||
|
SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review",
|
||||||
SIGN_SSH_KEY = "sign-ssh-key",
|
SIGN_SSH_KEY = "sign-ssh-key",
|
||||||
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
||||||
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
||||||
@@ -241,6 +252,7 @@ export enum EventType {
|
|||||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||||
DELETE_APP_CONNECTION = "delete-app-connection",
|
DELETE_APP_CONNECTION = "delete-app-connection",
|
||||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||||
|
CREATE_SECRET_REQUEST = "create-secret-request",
|
||||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||||
READ_SHARED_SECRET = "read-shared-secret",
|
READ_SHARED_SECRET = "read-shared-secret",
|
||||||
GET_SECRET_SYNCS = "get-secret-syncs",
|
GET_SECRET_SYNCS = "get-secret-syncs",
|
||||||
@@ -252,7 +264,26 @@ export enum EventType {
|
|||||||
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
||||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||||
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
|
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
|
||||||
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
|
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
|
||||||
|
CREATE_KMIP_CLIENT = "create-kmip-client",
|
||||||
|
UPDATE_KMIP_CLIENT = "update-kmip-client",
|
||||||
|
DELETE_KMIP_CLIENT = "delete-kmip-client",
|
||||||
|
GET_KMIP_CLIENT = "get-kmip-client",
|
||||||
|
GET_KMIP_CLIENTS = "get-kmip-clients",
|
||||||
|
CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate",
|
||||||
|
|
||||||
|
SETUP_KMIP = "setup-kmip",
|
||||||
|
GET_KMIP = "get-kmip",
|
||||||
|
REGISTER_KMIP_SERVER = "register-kmip-server",
|
||||||
|
|
||||||
|
KMIP_OPERATION_CREATE = "kmip-operation-create",
|
||||||
|
KMIP_OPERATION_GET = "kmip-operation-get",
|
||||||
|
KMIP_OPERATION_DESTROY = "kmip-operation-destroy",
|
||||||
|
KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes",
|
||||||
|
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
||||||
|
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||||
|
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||||
|
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@@ -275,6 +306,11 @@ interface ScimClientActorMetadata {}
|
|||||||
|
|
||||||
interface PlatformActorMetadata {}
|
interface PlatformActorMetadata {}
|
||||||
|
|
||||||
|
interface KmipClientActorMetadata {
|
||||||
|
clientId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface UnknownUserActorMetadata {}
|
interface UnknownUserActorMetadata {}
|
||||||
|
|
||||||
export interface UserActor {
|
export interface UserActor {
|
||||||
@@ -292,6 +328,11 @@ export interface PlatformActor {
|
|||||||
metadata: PlatformActorMetadata;
|
metadata: PlatformActorMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KmipClientActor {
|
||||||
|
type: ActorType.KMIP_CLIENT;
|
||||||
|
metadata: KmipClientActorMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UnknownUserActor {
|
export interface UnknownUserActor {
|
||||||
type: ActorType.UNKNOWN_USER;
|
type: ActorType.UNKNOWN_USER;
|
||||||
metadata: UnknownUserActorMetadata;
|
metadata: UnknownUserActorMetadata;
|
||||||
@@ -307,7 +348,7 @@ export interface ScimClientActor {
|
|||||||
metadata: ScimClientActorMetadata;
|
metadata: ScimClientActorMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
|
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor | KmipClientActor;
|
||||||
|
|
||||||
interface GetSecretsEvent {
|
interface GetSecretsEvent {
|
||||||
type: EventType.GET_SECRETS;
|
type: EventType.GET_SECRETS;
|
||||||
@@ -352,6 +393,7 @@ interface CreateSecretBatchEvent {
|
|||||||
secrets: Array<{
|
secrets: Array<{
|
||||||
secretId: string;
|
secretId: string;
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
|
secretPath?: string;
|
||||||
secretVersion: number;
|
secretVersion: number;
|
||||||
secretMetadata?: TSecretMetadata;
|
secretMetadata?: TSecretMetadata;
|
||||||
}>;
|
}>;
|
||||||
@@ -374,8 +416,14 @@ interface UpdateSecretBatchEvent {
|
|||||||
type: EventType.UPDATE_SECRETS;
|
type: EventType.UPDATE_SECRETS;
|
||||||
metadata: {
|
metadata: {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath?: string;
|
||||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number; secretMetadata?: TSecretMetadata }>;
|
secrets: Array<{
|
||||||
|
secretId: string;
|
||||||
|
secretKey: string;
|
||||||
|
secretVersion: number;
|
||||||
|
secretMetadata?: TSecretMetadata;
|
||||||
|
secretPath?: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,6 +1144,7 @@ interface CreateFolderEvent {
|
|||||||
folderId: string;
|
folderId: string;
|
||||||
folderName: string;
|
folderName: string;
|
||||||
folderPath: string;
|
folderPath: string;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1267,6 +1316,16 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecretApprovalRequestReview {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_REVIEW;
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: string;
|
||||||
|
reviewedBy: string;
|
||||||
|
status: ApprovalStatus;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SignSshKey {
|
interface SignSshKey {
|
||||||
type: EventType.SIGN_SSH_KEY;
|
type: EventType.SIGN_SSH_KEY;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1975,6 +2034,15 @@ interface CreateSharedSecretEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateSecretRequestEvent {
|
||||||
|
type: EventType.CREATE_SECRET_REQUEST;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
accessType: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface DeleteSharedSecretEvent {
|
interface DeleteSharedSecretEvent {
|
||||||
type: EventType.DELETE_SHARED_SECRET;
|
type: EventType.DELETE_SHARED_SECRET;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2084,6 +2152,139 @@ interface OidcGroupMembershipMappingRemoveUserEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateKmipClientEvent {
|
||||||
|
type: EventType.CREATE_KMIP_CLIENT;
|
||||||
|
metadata: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
permissions: KmipPermission[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateKmipClientEvent {
|
||||||
|
type: EventType.UPDATE_KMIP_CLIENT;
|
||||||
|
metadata: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
permissions: KmipPermission[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteKmipClientEvent {
|
||||||
|
type: EventType.DELETE_KMIP_CLIENT;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetKmipClientEvent {
|
||||||
|
type: EventType.GET_KMIP_CLIENT;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetKmipClientsEvent {
|
||||||
|
type: EventType.GET_KMIP_CLIENTS;
|
||||||
|
metadata: {
|
||||||
|
ids: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateKmipClientCertificateEvent {
|
||||||
|
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE;
|
||||||
|
metadata: {
|
||||||
|
clientId: string;
|
||||||
|
ttl: string;
|
||||||
|
keyAlgorithm: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KmipOperationGetEvent {
|
||||||
|
type: EventType.KMIP_OPERATION_GET;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KmipOperationDestroyEvent {
|
||||||
|
type: EventType.KMIP_OPERATION_DESTROY;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KmipOperationCreateEvent {
|
||||||
|
type: EventType.KMIP_OPERATION_CREATE;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
algorithm: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KmipOperationGetAttributesEvent {
|
||||||
|
type: EventType.KMIP_OPERATION_GET_ATTRIBUTES;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KmipOperationActivateEvent {
|
||||||
|
type: EventType.KMIP_OPERATION_ACTIVATE;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KmipOperationRevokeEvent {
|
||||||
|
type: EventType.KMIP_OPERATION_REVOKE;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KmipOperationLocateEvent {
|
||||||
|
type: EventType.KMIP_OPERATION_LOCATE;
|
||||||
|
metadata: {
|
||||||
|
ids: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KmipOperationRegisterEvent {
|
||||||
|
type: EventType.KMIP_OPERATION_REGISTER;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
algorithm: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SetupKmipEvent {
|
||||||
|
type: EventType.SETUP_KMIP;
|
||||||
|
metadata: {
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetKmipEvent {
|
||||||
|
type: EventType.GET_KMIP;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegisterKmipServerEvent {
|
||||||
|
type: EventType.REGISTER_KMIP_SERVER;
|
||||||
|
metadata: {
|
||||||
|
serverCertificateSerialNumber: string;
|
||||||
|
hostnamesOrIps: string;
|
||||||
|
commonName: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
ttl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -2275,4 +2476,23 @@ export type Event =
|
|||||||
| SecretSyncImportSecretsEvent
|
| SecretSyncImportSecretsEvent
|
||||||
| SecretSyncRemoveSecretsEvent
|
| SecretSyncRemoveSecretsEvent
|
||||||
| OidcGroupMembershipMappingAssignUserEvent
|
| OidcGroupMembershipMappingAssignUserEvent
|
||||||
| OidcGroupMembershipMappingRemoveUserEvent;
|
| OidcGroupMembershipMappingRemoveUserEvent
|
||||||
|
| CreateKmipClientEvent
|
||||||
|
| UpdateKmipClientEvent
|
||||||
|
| DeleteKmipClientEvent
|
||||||
|
| GetKmipClientEvent
|
||||||
|
| GetKmipClientsEvent
|
||||||
|
| CreateKmipClientCertificateEvent
|
||||||
|
| SetupKmipEvent
|
||||||
|
| GetKmipEvent
|
||||||
|
| RegisterKmipServerEvent
|
||||||
|
| KmipOperationGetEvent
|
||||||
|
| KmipOperationDestroyEvent
|
||||||
|
| KmipOperationCreateEvent
|
||||||
|
| KmipOperationGetAttributesEvent
|
||||||
|
| KmipOperationActivateEvent
|
||||||
|
| KmipOperationRevokeEvent
|
||||||
|
| KmipOperationLocateEvent
|
||||||
|
| KmipOperationRegisterEvent
|
||||||
|
| CreateSecretRequestEvent
|
||||||
|
| SecretApprovalRequestReview;
|
||||||
|
@@ -1,20 +1,31 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { getDbConnectionHost } from "@app/lib/knex";
|
import { getDbConnectionHost } from "@app/lib/knex";
|
||||||
|
|
||||||
export const verifyHostInputValidity = (host: string) => {
|
export const verifyHostInputValidity = (host: string, isGateway = false) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||||
|
// no need for validation when it's dev
|
||||||
|
if (appCfg.NODE_ENV === "development") return;
|
||||||
|
|
||||||
|
if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
appCfg.isCloud &&
|
appCfg.isCloud &&
|
||||||
|
!isGateway &&
|
||||||
// localhost
|
// localhost
|
||||||
// internal ips
|
// internal ips
|
||||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
(host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||||
)
|
)
|
||||||
throw new BadRequestError({ message: "Invalid db host" });
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
|
||||||
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
|
if (
|
||||||
|
host === "localhost" ||
|
||||||
|
host === "127.0.0.1" ||
|
||||||
|
(dbHost?.length === host.length && crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host)))
|
||||||
|
) {
|
||||||
throw new BadRequestError({ message: "Invalid db host" });
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -16,6 +16,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
|||||||
|
|
||||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||||
|
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
|
||||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||||
import {
|
import {
|
||||||
DynamicSecretStatus,
|
DynamicSecretStatus,
|
||||||
@@ -44,6 +45,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||||
@@ -57,7 +59,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
projectGatewayDAL
|
||||||
}: TDynamicSecretServiceFactoryDep) => {
|
}: TDynamicSecretServiceFactoryDep) => {
|
||||||
const create = async ({
|
const create = async ({
|
||||||
path,
|
path,
|
||||||
@@ -108,6 +111,18 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
||||||
|
|
||||||
|
let selectedGatewayId: string | null = null;
|
||||||
|
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
|
||||||
|
const projectGatewayId = inputs.projectGatewayId as string;
|
||||||
|
|
||||||
|
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||||
|
if (!projectGateway)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project gateway with ${projectGatewayId} not found`
|
||||||
|
});
|
||||||
|
selectedGatewayId = projectGateway.id;
|
||||||
|
}
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
@@ -123,7 +138,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
maxTTL,
|
maxTTL,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
name
|
name,
|
||||||
|
projectGatewayId: selectedGatewayId
|
||||||
});
|
});
|
||||||
return dynamicSecretCfg;
|
return dynamicSecretCfg;
|
||||||
};
|
};
|
||||||
@@ -195,6 +211,23 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||||
|
|
||||||
|
let selectedGatewayId: string | null = null;
|
||||||
|
if (
|
||||||
|
updatedInput &&
|
||||||
|
typeof updatedInput === "object" &&
|
||||||
|
"projectGatewayId" in updatedInput &&
|
||||||
|
updatedInput?.projectGatewayId
|
||||||
|
) {
|
||||||
|
const projectGatewayId = updatedInput.projectGatewayId as string;
|
||||||
|
|
||||||
|
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||||
|
if (!projectGateway)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project gateway with ${projectGatewayId} not found`
|
||||||
|
});
|
||||||
|
selectedGatewayId = projectGateway.id;
|
||||||
|
}
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
@@ -204,7 +237,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
defaultTTL,
|
defaultTTL,
|
||||||
name: newName ?? name,
|
name: newName ?? name,
|
||||||
status: null,
|
status: null,
|
||||||
statusDetails: null
|
statusDetails: null,
|
||||||
|
projectGatewayId: selectedGatewayId
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedDynamicCfg;
|
return updatedDynamicCfg;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||||
|
|
||||||
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||||
import { AwsIamProvider } from "./aws-iam";
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||||
@@ -16,8 +17,14 @@ import { SapHanaProvider } from "./sap-hana";
|
|||||||
import { SqlDatabaseProvider } from "./sql-database";
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
import { TotpProvider } from "./totp";
|
import { TotpProvider } from "./totp";
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
type TBuildDynamicSecretProviderDTO = {
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildDynamicSecretProviders = ({
|
||||||
|
gatewayService
|
||||||
|
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||||
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
|
||||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||||
|
@@ -103,7 +103,8 @@ export const DynamicSecretSqlDBSchema = z.object({
|
|||||||
creationStatement: z.string().trim(),
|
creationStatement: z.string().trim(),
|
||||||
revocationStatement: z.string().trim(),
|
revocationStatement: z.string().trim(),
|
||||||
renewStatement: z.string().trim().optional(),
|
renewStatement: z.string().trim().optional(),
|
||||||
ca: z.string().optional()
|
ca: z.string().optional(),
|
||||||
|
projectGatewayId: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DynamicSecretCassandraSchema = z.object({
|
export const DynamicSecretCassandraSchema = z.object({
|
||||||
|
@@ -3,8 +3,10 @@ import knex from "knex";
|
|||||||
import { customAlphabet } from "nanoid";
|
import { customAlphabet } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
@@ -25,10 +27,14 @@ const generateUsername = (provider: SqlProviders) => {
|
|||||||
return alphaNumericNanoId(32);
|
return alphaNumericNanoId(32);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
type TSqlDatabaseProviderDTO = {
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||||
verifyHostInputValidity(providerInputs.host);
|
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
|
||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,7 +51,6 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
user: providerInputs.username,
|
user: providerInputs.username,
|
||||||
password: providerInputs.password,
|
password: providerInputs.password,
|
||||||
ssl,
|
ssl,
|
||||||
pool: { min: 0, max: 1 },
|
|
||||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
||||||
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
||||||
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||||
@@ -61,61 +66,112 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const gatewayProxyWrapper = async (
|
||||||
|
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
||||||
|
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||||
|
) => {
|
||||||
|
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
|
||||||
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
await withGatewayProxy(
|
||||||
|
async (port) => {
|
||||||
|
await gatewayCallback("localhost", port);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetHost: providerInputs.host,
|
||||||
|
targetPort: providerInputs.port,
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
identityId: relayDetails.identityId,
|
||||||
|
orgId: relayDetails.orgId,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: relayDetails.certChain,
|
||||||
|
cert: relayDetails.certificate,
|
||||||
|
key: relayDetails.privateKey.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
let isConnected = false;
|
||||||
// oracle needs from keyword
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
|
// oracle needs from keyword
|
||||||
|
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||||
|
|
||||||
const isConnected = await db.raw(testStatement).then(() => true);
|
isConnected = await db.raw(testStatement).then(() => true);
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (providerInputs.projectGatewayId) {
|
||||||
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
|
}
|
||||||
return isConnected;
|
return isConnected;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 username = generateUsername(providerInputs.client);
|
const username = generateUsername(providerInputs.client);
|
||||||
const password = generatePassword(providerInputs.client);
|
const password = generatePassword(providerInputs.client);
|
||||||
const { database } = providerInputs;
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
|
try {
|
||||||
|
const { database } = providerInputs;
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
expiration,
|
expiration,
|
||||||
database
|
database
|
||||||
});
|
});
|
||||||
|
|
||||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
await tx.raw(query);
|
await tx.raw(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await db.destroy();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
await db.destroy();
|
if (providerInputs.projectGatewayId) {
|
||||||
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
|
}
|
||||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
};
|
};
|
||||||
|
|
||||||
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 username = entityId;
|
const username = entityId;
|
||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
try {
|
||||||
await db.transaction(async (tx) => {
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||||
for (const query of queries) {
|
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||||
// eslint-disable-next-line
|
await db.transaction(async (tx) => {
|
||||||
await tx.raw(query);
|
for (const query of queries) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
await tx.raw(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await db.destroy();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
if (providerInputs.projectGatewayId) {
|
||||||
await db.destroy();
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
|
}
|
||||||
return { entityId: username };
|
return { entityId: username };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,28 +179,35 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
if (!providerInputs.renewStatement) return { entityId };
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
const db = await $getClient(providerInputs);
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
const { database } = providerInputs;
|
||||||
|
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||||
const { database } = providerInputs;
|
username: entityId,
|
||||||
|
expiration,
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
database
|
||||||
username: entityId,
|
|
||||||
expiration,
|
|
||||||
database
|
|
||||||
});
|
|
||||||
|
|
||||||
if (renewStatement) {
|
|
||||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
for (const query of queries) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
await tx.raw(query);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
if (renewStatement) {
|
||||||
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
for (const query of queries) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
await tx.raw(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await db.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (providerInputs.projectGatewayId) {
|
||||||
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.destroy();
|
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
86
backend/src/ee/services/gateway/gateway-dal.ts
Normal file
86
backend/src/ee/services/gateway/gateway-dal.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import {
|
||||||
|
buildFindFilter,
|
||||||
|
ormify,
|
||||||
|
selectAllTableCols,
|
||||||
|
sqlNestRelationships,
|
||||||
|
TFindFilter,
|
||||||
|
TFindOpt
|
||||||
|
} from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
|
||||||
|
|
||||||
|
export const gatewayDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.Gateway);
|
||||||
|
|
||||||
|
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db)(TableName.Gateway)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter(filter))
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||||
|
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||||
|
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`)
|
||||||
|
.select(selectAllTableCols(TableName.Gateway))
|
||||||
|
.select(
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
|
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
|
||||||
|
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||||
|
);
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.offset(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
return sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (data) => ({
|
||||||
|
...GatewaysSchema.parse(data),
|
||||||
|
identity: { id: data.identityId, name: data.identityName }
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "projectId",
|
||||||
|
label: "projects" as const,
|
||||||
|
mapper: ({ projectId, projectName, projectSlug }) => ({
|
||||||
|
id: projectId,
|
||||||
|
name: projectName,
|
||||||
|
slug: projectSlug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db)(TableName.Gateway)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||||
|
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Gateway))
|
||||||
|
.select(
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
|
||||||
|
)
|
||||||
|
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...orm, find, findByProjectId };
|
||||||
|
};
|
652
backend/src/ee/services/gateway/gateway-service.ts
Normal file
652
backend/src/ee/services/gateway/gateway-service.ts
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { pingGatewayAndVerify } from "@app/lib/gateway";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { getTurnCredentials } from "@app/lib/turn/credentials";
|
||||||
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
|
import {
|
||||||
|
createSerialNumber,
|
||||||
|
keyAlgorithmToAlgCfg
|
||||||
|
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { TGatewayDALFactory } from "./gateway-dal";
|
||||||
|
import {
|
||||||
|
TExchangeAllocatedRelayAddressDTO,
|
||||||
|
TGetGatewayByIdDTO,
|
||||||
|
TGetProjectGatewayByIdDTO,
|
||||||
|
THeartBeatDTO,
|
||||||
|
TListGatewaysDTO,
|
||||||
|
TUpdateGatewayByIdDTO
|
||||||
|
} from "./gateway-types";
|
||||||
|
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
|
||||||
|
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
|
||||||
|
|
||||||
|
type TGatewayServiceFactoryDep = {
|
||||||
|
gatewayDAL: TGatewayDALFactory;
|
||||||
|
projectGatewayDAL: TProjectGatewayDALFactory;
|
||||||
|
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGatewayServiceFactory = ReturnType<typeof gatewayServiceFactory>;
|
||||||
|
const TURN_SERVER_CREDENTIALS_SCHEMA = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gatewayServiceFactory = ({
|
||||||
|
gatewayDAL,
|
||||||
|
licenseService,
|
||||||
|
kmsService,
|
||||||
|
permissionService,
|
||||||
|
orgGatewayConfigDAL,
|
||||||
|
keyStore,
|
||||||
|
projectGatewayDAL
|
||||||
|
}: TGatewayServiceFactoryDep) => {
|
||||||
|
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||||
|
// if (!licenseService.onPremFeatures.gateway) {
|
||||||
|
// throw new BadRequestError({
|
||||||
|
// message:
|
||||||
|
// "Gateway handshake failed due to instance plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||||
|
if (!orgLicensePlan.gateway) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.CreateGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||||
|
const TURN_CRED_EXPIRY = 10 * 60; // 10 minutes
|
||||||
|
|
||||||
|
const envCfg = getConfig();
|
||||||
|
await $validateOrgAccessToGateway(actorOrgId, actorId, actorAuthMethod);
|
||||||
|
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envCfg.GATEWAY_RELAY_AUTH_SECRET || !envCfg.GATEWAY_RELAY_ADDRESS || !envCfg.GATEWAY_RELAY_REALM) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Gateway handshake failed due to missing instance configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let turnServerUsername = "";
|
||||||
|
let turnServerPassword = "";
|
||||||
|
// keep it in redis for 5mins to avoid generating so many credentials
|
||||||
|
const previousCredential = await keyStore.getItem(KeyStorePrefixes.GatewayIdentityCredential(actorId));
|
||||||
|
if (previousCredential) {
|
||||||
|
const el = await TURN_SERVER_CREDENTIALS_SCHEMA.parseAsync(
|
||||||
|
JSON.parse(decryptor({ cipherTextBlob: Buffer.from(previousCredential, "hex") }).toString())
|
||||||
|
);
|
||||||
|
turnServerUsername = el.username;
|
||||||
|
turnServerPassword = el.password;
|
||||||
|
} else {
|
||||||
|
const el = getTurnCredentials(actorId, envCfg.GATEWAY_RELAY_AUTH_SECRET);
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.GatewayIdentityCredential(actorId),
|
||||||
|
TURN_CRED_EXPIRY,
|
||||||
|
encryptor({
|
||||||
|
plainText: Buffer.from(JSON.stringify({ username: el.username, password: el.password }))
|
||||||
|
}).cipherTextBlob.toString("hex")
|
||||||
|
);
|
||||||
|
turnServerUsername = el.username;
|
||||||
|
turnServerPassword = el.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
turnServerUsername,
|
||||||
|
turnServerPassword,
|
||||||
|
turnServerRealm: envCfg.GATEWAY_RELAY_REALM,
|
||||||
|
turnServerAddress: envCfg.GATEWAY_RELAY_ADDRESS,
|
||||||
|
infisicalStaticIp: envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeAllocatedRelayAddress = async ({
|
||||||
|
identityId,
|
||||||
|
identityOrg,
|
||||||
|
relayAddress,
|
||||||
|
identityOrgAuthMethod
|
||||||
|
}: TExchangeAllocatedRelayAddressDTO) => {
|
||||||
|
await $validateOrgAccessToGateway(identityOrg, identityId, identityOrgAuthMethod);
|
||||||
|
const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: identityOrg
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.transaction(async (tx) => {
|
||||||
|
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayRootCaInit(identityOrg)]);
|
||||||
|
const existingGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: identityOrg });
|
||||||
|
if (existingGatewayConfig) return existingGatewayConfig;
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||||
|
// generate root CA
|
||||||
|
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const rootCaSerialNumber = createSerialNumber();
|
||||||
|
const rootCaSkObj = crypto.KeyObject.from(rootCaKeys.privateKey);
|
||||||
|
const rootCaIssuedAt = new Date();
|
||||||
|
const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048;
|
||||||
|
const rootCaExpiration = new Date(new Date().setFullYear(2045));
|
||||||
|
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||||
|
name: `O=${identityOrg},CN=Infisical Gateway Root CA`,
|
||||||
|
serialNumber: rootCaSerialNumber,
|
||||||
|
notBefore: rootCaIssuedAt,
|
||||||
|
notAfter: rootCaExpiration,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
keys: rootCaKeys,
|
||||||
|
extensions: [
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate client ca
|
||||||
|
const clientCaSerialNumber = createSerialNumber();
|
||||||
|
const clientCaIssuedAt = new Date();
|
||||||
|
const clientCaExpiration = new Date(new Date().setFullYear(2045));
|
||||||
|
const clientCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const clientCaSkObj = crypto.KeyObject.from(clientCaKeys.privateKey);
|
||||||
|
|
||||||
|
const clientCaCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber: clientCaSerialNumber,
|
||||||
|
subject: `O=${identityOrg},CN=Client Intermediate CA`,
|
||||||
|
issuer: rootCaCert.subject,
|
||||||
|
notBefore: clientCaIssuedAt,
|
||||||
|
notAfter: clientCaExpiration,
|
||||||
|
signingKey: rootCaKeys.privateKey,
|
||||||
|
publicKey: clientCaKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags.keyCertSign |
|
||||||
|
x509.KeyUsageFlags.cRLSign |
|
||||||
|
x509.KeyUsageFlags.digitalSignature |
|
||||||
|
x509.KeyUsageFlags.keyEncipherment,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.BasicConstraintsExtension(true, 0, true),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const clientCertSerialNumber = createSerialNumber();
|
||||||
|
const clientCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber: clientCertSerialNumber,
|
||||||
|
subject: `O=${identityOrg},OU=gateway-client,CN=cloud`,
|
||||||
|
issuer: clientCaCert.subject,
|
||||||
|
notAfter: clientCaExpiration,
|
||||||
|
notBefore: clientCaIssuedAt,
|
||||||
|
signingKey: clientCaKeys.privateKey,
|
||||||
|
publicKey: clientKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(clientCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const clientSkObj = crypto.KeyObject.from(clientKeys.privateKey);
|
||||||
|
|
||||||
|
// generate gateway ca
|
||||||
|
const gatewayCaSerialNumber = createSerialNumber();
|
||||||
|
const gatewayCaIssuedAt = new Date();
|
||||||
|
const gatewayCaExpiration = new Date(new Date().setFullYear(2045));
|
||||||
|
const gatewayCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const gatewayCaSkObj = crypto.KeyObject.from(gatewayCaKeys.privateKey);
|
||||||
|
const gatewayCaCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber: gatewayCaSerialNumber,
|
||||||
|
subject: `O=${identityOrg},CN=Gateway CA`,
|
||||||
|
issuer: rootCaCert.subject,
|
||||||
|
notBefore: gatewayCaIssuedAt,
|
||||||
|
notAfter: gatewayCaExpiration,
|
||||||
|
signingKey: rootCaKeys.privateKey,
|
||||||
|
publicKey: gatewayCaKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags.keyCertSign |
|
||||||
|
x509.KeyUsageFlags.cRLSign |
|
||||||
|
x509.KeyUsageFlags.digitalSignature |
|
||||||
|
x509.KeyUsageFlags.keyEncipherment,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.BasicConstraintsExtension(true, 0, true),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(gatewayCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return orgGatewayConfigDAL.create({
|
||||||
|
orgId: identityOrg,
|
||||||
|
rootCaIssuedAt,
|
||||||
|
rootCaExpiration,
|
||||||
|
rootCaSerialNumber,
|
||||||
|
rootCaKeyAlgorithm,
|
||||||
|
encryptedRootCaPrivateKey: orgKmsEncryptor({
|
||||||
|
plainText: rootCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedRootCaCertificate: orgKmsEncryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
|
||||||
|
|
||||||
|
clientCaIssuedAt,
|
||||||
|
clientCaExpiration,
|
||||||
|
clientCaSerialNumber,
|
||||||
|
encryptedClientCaPrivateKey: orgKmsEncryptor({
|
||||||
|
plainText: clientCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedClientCaCertificate: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(clientCaCert.rawData)
|
||||||
|
}).cipherTextBlob,
|
||||||
|
|
||||||
|
clientCertIssuedAt: clientCaIssuedAt,
|
||||||
|
clientCertExpiration: clientCaExpiration,
|
||||||
|
clientCertKeyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||||
|
clientCertSerialNumber,
|
||||||
|
encryptedClientPrivateKey: orgKmsEncryptor({
|
||||||
|
plainText: clientSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedClientCertificate: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(clientCert.rawData)
|
||||||
|
}).cipherTextBlob,
|
||||||
|
|
||||||
|
gatewayCaIssuedAt,
|
||||||
|
gatewayCaExpiration,
|
||||||
|
gatewayCaSerialNumber,
|
||||||
|
encryptedGatewayCaPrivateKey: orgKmsEncryptor({
|
||||||
|
plainText: gatewayCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedGatewayCaCertificate: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(gatewayCaCert.rawData)
|
||||||
|
}).cipherTextBlob
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const clientCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedClientCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const gatewayCaAlg = keyAlgorithmToAlgCfg(orgGatewayConfig.rootCaKeyAlgorithm as CertKeyAlgorithm);
|
||||||
|
const gatewayCaSkObj = crypto.createPrivateKey({
|
||||||
|
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedGatewayCaPrivateKey }),
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
const gatewayCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const gatewayCaPrivateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
gatewayCaSkObj.export({ format: "der", type: "pkcs8" }),
|
||||||
|
gatewayCaAlg,
|
||||||
|
true,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||||
|
const gatewayKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const certIssuedAt = new Date();
|
||||||
|
// then need to periodically init
|
||||||
|
const certExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1));
|
||||||
|
|
||||||
|
const extensions: x509.Extension[] = [
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(gatewayCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(gatewayKeys.publicKey),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
|
||||||
|
// san
|
||||||
|
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: relayAddress.split(":")[0] }], false)
|
||||||
|
];
|
||||||
|
|
||||||
|
const serialNumber = createSerialNumber();
|
||||||
|
const privateKey = crypto.KeyObject.from(gatewayKeys.privateKey);
|
||||||
|
const gatewayCertificate = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: `CN=${identityId},O=${identityOrg},OU=Gateway`,
|
||||||
|
issuer: gatewayCaCert.subject,
|
||||||
|
notBefore: certIssuedAt,
|
||||||
|
notAfter: certExpireAt,
|
||||||
|
signingKey: gatewayCaPrivateKey,
|
||||||
|
publicKey: gatewayKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
// just for local development
|
||||||
|
const formatedRelayAddress =
|
||||||
|
appCfg.NODE_ENV === "development" ? relayAddress.replace("127.0.0.1", "host.docker.internal") : relayAddress;
|
||||||
|
|
||||||
|
await gatewayDAL.transaction(async (tx) => {
|
||||||
|
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]);
|
||||||
|
const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||||
|
|
||||||
|
if (existingGateway) {
|
||||||
|
return gatewayDAL.updateById(existingGateway.id, {
|
||||||
|
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||||
|
issuedAt: certIssuedAt,
|
||||||
|
expiration: certExpireAt,
|
||||||
|
serialNumber,
|
||||||
|
relayAddress: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(formatedRelayAddress)
|
||||||
|
}).cipherTextBlob
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return gatewayDAL.create({
|
||||||
|
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||||
|
issuedAt: certIssuedAt,
|
||||||
|
expiration: certExpireAt,
|
||||||
|
serialNumber,
|
||||||
|
relayAddress: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(formatedRelayAddress)
|
||||||
|
}).cipherTextBlob,
|
||||||
|
identityId,
|
||||||
|
orgGatewayRootCaId: orgGatewayConfig.id,
|
||||||
|
name: `gateway-${alphaNumericNanoId(6).toLowerCase()}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const gatewayCertificateChain = `${clientCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string,
|
||||||
|
certificate: gatewayCertificate.toString("pem"),
|
||||||
|
certificateChain: gatewayCertificateChain
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = async ({ orgPermission }: THeartBeatDTO) => {
|
||||||
|
await $validateOrgAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) throw new NotFoundError({ message: `Identity with ID ${orgPermission.id} not found.` });
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.find({ identityId: orgPermission.id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${orgPermission.id} not found.` });
|
||||||
|
|
||||||
|
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: orgGatewayConfig.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const gatewayCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const clientCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const privateKey = crypto
|
||||||
|
.createPrivateKey({
|
||||||
|
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
})
|
||||||
|
.export({ type: "pkcs8", format: "pem" });
|
||||||
|
|
||||||
|
const relayAddress = orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString();
|
||||||
|
const [relayHost, relayPort] = relayAddress.split(":");
|
||||||
|
|
||||||
|
await pingGatewayAndVerify({
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
tlsOptions: {
|
||||||
|
key: privateKey.toString(),
|
||||||
|
ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||||
|
cert: clientCert.toString("pem")
|
||||||
|
},
|
||||||
|
identityId: orgPermission.id,
|
||||||
|
orgId: orgPermission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await gatewayDAL.updateById(gateway.id, { heartbeat: new Date() });
|
||||||
|
};
|
||||||
|
|
||||||
|
const listGateways = async ({ orgPermission }: TListGatewaysDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
orgPermission.type,
|
||||||
|
orgPermission.id,
|
||||||
|
orgPermission.orgId,
|
||||||
|
orgPermission.authMethod,
|
||||||
|
orgPermission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.ListGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) return [];
|
||||||
|
|
||||||
|
const gateways = await gatewayDAL.find({
|
||||||
|
orgGatewayRootCaId: orgGatewayConfig.id
|
||||||
|
});
|
||||||
|
return gateways;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
orgPermission.type,
|
||||||
|
orgPermission.id,
|
||||||
|
orgPermission.orgId,
|
||||||
|
orgPermission.authMethod,
|
||||||
|
orgPermission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.ListGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.find({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
return gateway;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
orgPermission.type,
|
||||||
|
orgPermission.id,
|
||||||
|
orgPermission.orgId,
|
||||||
|
orgPermission.authMethod,
|
||||||
|
orgPermission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.EditGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
if (projectIds) {
|
||||||
|
await projectGatewayDAL.transaction(async (tx) => {
|
||||||
|
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
|
||||||
|
await projectGatewayDAL.insertMany(
|
||||||
|
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return gateway;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
orgPermission.type,
|
||||||
|
orgPermission.id,
|
||||||
|
orgPermission.orgId,
|
||||||
|
orgPermission.authMethod,
|
||||||
|
orgPermission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.DeleteGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.delete({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
return gateway;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
projectId,
|
||||||
|
actor: projectPermission.type,
|
||||||
|
actorId: projectPermission.id,
|
||||||
|
actorOrgId: projectPermission.orgId,
|
||||||
|
actorAuthMethod: projectPermission.authMethod,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
const gateways = await gatewayDAL.findByProjectId(projectId);
|
||||||
|
return gateways;
|
||||||
|
};
|
||||||
|
|
||||||
|
// this has no permission check and used for dynamic secrets directly
|
||||||
|
// assumes permission check is already done
|
||||||
|
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
|
||||||
|
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
|
||||||
|
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
|
||||||
|
|
||||||
|
const { gatewayId } = projectGateway;
|
||||||
|
const gateway = await gatewayDAL.findById(gatewayId);
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
|
||||||
|
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId);
|
||||||
|
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: orgGatewayConfig.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const gatewayCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const clientCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const clientSkObj = crypto.createPrivateKey({
|
||||||
|
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
relayAddress: orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(),
|
||||||
|
privateKey: clientSkObj.export({ type: "pkcs8", format: "pem" }),
|
||||||
|
certificate: clientCert.toString("pem"),
|
||||||
|
certChain: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||||
|
identityId: gateway.identityId,
|
||||||
|
orgId: orgGatewayConfig.orgId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getGatewayRelayDetails,
|
||||||
|
exchangeAllocatedRelayAddress,
|
||||||
|
listGateways,
|
||||||
|
getGatewayById,
|
||||||
|
updateGatewayById,
|
||||||
|
deleteGatewayById,
|
||||||
|
getProjectGateways,
|
||||||
|
fnGetGatewayClientTls,
|
||||||
|
heartbeat
|
||||||
|
};
|
||||||
|
};
|
39
backend/src/ee/services/gateway/gateway-types.ts
Normal file
39
backend/src/ee/services/gateway/gateway-types.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export type TExchangeAllocatedRelayAddressDTO = {
|
||||||
|
identityId: string;
|
||||||
|
identityOrg: string;
|
||||||
|
identityOrgAuthMethod: ActorAuthMethod;
|
||||||
|
relayAddress: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListGatewaysDTO = {
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetGatewayByIdDTO = {
|
||||||
|
id: string;
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateGatewayByIdDTO = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
projectIds?: string[];
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteGatewayByIdDTO = {
|
||||||
|
id: string;
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetProjectGatewayByIdDTO = {
|
||||||
|
projectId: string;
|
||||||
|
projectPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THeartBeatDTO = {
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
10
backend/src/ee/services/gateway/org-gateway-config-dal.ts
Normal file
10
backend/src/ee/services/gateway/org-gateway-config-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TOrgGatewayConfigDALFactory = ReturnType<typeof orgGatewayConfigDALFactory>;
|
||||||
|
|
||||||
|
export const orgGatewayConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.OrgGatewayConfig);
|
||||||
|
return orm;
|
||||||
|
};
|
10
backend/src/ee/services/gateway/project-gateway-dal.ts
Normal file
10
backend/src/ee/services/gateway/project-gateway-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
|
||||||
|
|
||||||
|
export const projectGatewayDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.ProjectGateway);
|
||||||
|
return orm;
|
||||||
|
};
|
11
backend/src/ee/services/kmip/kmip-client-certificate-dal.ts
Normal file
11
backend/src/ee/services/kmip/kmip-client-certificate-dal.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TKmipClientCertificateDALFactory = ReturnType<typeof kmipClientCertificateDALFactory>;
|
||||||
|
|
||||||
|
export const kmipClientCertificateDALFactory = (db: TDbClient) => {
|
||||||
|
const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates);
|
||||||
|
|
||||||
|
return kmipClientCertOrm;
|
||||||
|
};
|
86
backend/src/ee/services/kmip/kmip-client-dal.ts
Normal file
86
backend/src/ee/services/kmip/kmip-client-dal.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TKmipClients } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { KmipClientOrderBy } from "./kmip-types";
|
||||||
|
|
||||||
|
export type TKmipClientDALFactory = ReturnType<typeof kmipClientDALFactory>;
|
||||||
|
|
||||||
|
export const kmipClientDALFactory = (db: TDbClient) => {
|
||||||
|
const kmipClientOrm = ormify(db, TableName.KmipClient);
|
||||||
|
|
||||||
|
const findByProjectAndClientId = async (projectId: string, clientId: string) => {
|
||||||
|
try {
|
||||||
|
const client = await db
|
||||||
|
.replicaNode()(TableName.KmipClient)
|
||||||
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.KmipClient}.projectId`)
|
||||||
|
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
|
||||||
|
.where({
|
||||||
|
[`${TableName.KmipClient}.projectId` as "projectId"]: projectId,
|
||||||
|
[`${TableName.KmipClient}.id` as "id"]: clientId
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.KmipClient))
|
||||||
|
.select(db.ref("id").withSchema(TableName.Organization).as("orgId"))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return client;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by project and client ID" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByProjectId = async (
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
offset = 0,
|
||||||
|
limit,
|
||||||
|
orderBy = KmipClientOrderBy.Name,
|
||||||
|
orderDirection = OrderByDirection.ASC,
|
||||||
|
search
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: KmipClientOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.KmipClient)
|
||||||
|
.where("projectId", projectId)
|
||||||
|
.where((qb) => {
|
||||||
|
if (search) {
|
||||||
|
void qb.whereILike("name", `%${search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select<
|
||||||
|
(TKmipClients & {
|
||||||
|
total_count: number;
|
||||||
|
})[]
|
||||||
|
>(selectAllTableCols(TableName.KmipClient), db.raw(`count(*) OVER() as total_count`))
|
||||||
|
.orderBy(orderBy, orderDirection);
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
void query.limit(limit).offset(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await query;
|
||||||
|
|
||||||
|
return { kmipClients: data, totalCount: Number(data?.[0]?.total_count ?? 0) };
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find KMIP clients by project id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...kmipClientOrm,
|
||||||
|
findByProjectId,
|
||||||
|
findByProjectAndClientId
|
||||||
|
};
|
||||||
|
};
|
11
backend/src/ee/services/kmip/kmip-enum.ts
Normal file
11
backend/src/ee/services/kmip/kmip-enum.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export enum KmipPermission {
|
||||||
|
Create = "create",
|
||||||
|
Locate = "locate",
|
||||||
|
Check = "check",
|
||||||
|
Get = "get",
|
||||||
|
GetAttributes = "get-attributes",
|
||||||
|
Activate = "activate",
|
||||||
|
Revoke = "revoke",
|
||||||
|
Destroy = "destroy",
|
||||||
|
Register = "register"
|
||||||
|
}
|
422
backend/src/ee/services/kmip/kmip-operation-service.ts
Normal file
422
backend/src/ee/services/kmip/kmip-operation-service.ts
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
|
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { TKmipClientDALFactory } from "./kmip-client-dal";
|
||||||
|
import { KmipPermission } from "./kmip-enum";
|
||||||
|
import {
|
||||||
|
TKmipCreateDTO,
|
||||||
|
TKmipDestroyDTO,
|
||||||
|
TKmipGetAttributesDTO,
|
||||||
|
TKmipGetDTO,
|
||||||
|
TKmipLocateDTO,
|
||||||
|
TKmipRegisterDTO,
|
||||||
|
TKmipRevokeDTO
|
||||||
|
} from "./kmip-types";
|
||||||
|
|
||||||
|
type TKmipOperationServiceFactoryDep = {
|
||||||
|
kmsService: TKmsServiceFactory;
|
||||||
|
kmsDAL: TKmsKeyDALFactory;
|
||||||
|
kmipClientDAL: TKmipClientDALFactory;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId" | "findById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TKmipOperationServiceFactory = ReturnType<typeof kmipOperationServiceFactory>;
|
||||||
|
|
||||||
|
export const kmipOperationServiceFactory = ({
|
||||||
|
kmsService,
|
||||||
|
kmsDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmipClientDAL,
|
||||||
|
permissionService
|
||||||
|
}: TKmipOperationServiceFactoryDep) => {
|
||||||
|
const create = async ({
|
||||||
|
projectId,
|
||||||
|
clientId,
|
||||||
|
algorithm,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TKmipCreateDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.findOne({
|
||||||
|
id: clientId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipClient.permissions?.includes(KmipPermission.Create)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client does not have sufficient permission to perform KMIP create"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const kmsKey = await kmsService.generateKmsKey({
|
||||||
|
encryptionAlgorithm: algorithm,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
projectId,
|
||||||
|
isReserved: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmsKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.findOne({
|
||||||
|
id: clientId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipClient.permissions?.includes(KmipPermission.Destroy)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client does not have sufficient permission to perform KMIP destroy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await kmsDAL.findOne({
|
||||||
|
id,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.isReserved) {
|
||||||
|
throw new BadRequestError({ message: "Cannot destroy reserved keys" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
|
||||||
|
if (!completeKeyDetails.internalKms) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot destroy external keys"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!completeKeyDetails.isDisabled) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot destroy active keys"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const kms = kmsDAL.deleteById(id);
|
||||||
|
|
||||||
|
return kms;
|
||||||
|
};
|
||||||
|
|
||||||
|
const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.findOne({
|
||||||
|
id: clientId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipClient.permissions?.includes(KmipPermission.Get)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client does not have sufficient permission to perform KMIP get"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await kmsDAL.findOne({
|
||||||
|
id,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.isReserved) {
|
||||||
|
throw new BadRequestError({ message: "Cannot get reserved keys" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
|
||||||
|
|
||||||
|
if (!completeKeyDetails.internalKms) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot get external keys"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const kmsKey = await kmsService.getKeyMaterial({
|
||||||
|
kmsId: key.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key.id,
|
||||||
|
value: kmsKey.toString("base64"),
|
||||||
|
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
||||||
|
isActive: !key.isDisabled,
|
||||||
|
createdAt: key.createdAt,
|
||||||
|
updatedAt: key.updatedAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.findOne({
|
||||||
|
id: clientId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipClient.permissions?.includes(KmipPermission.Activate)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client does not have sufficient permission to perform KMIP activate"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await kmsDAL.findOne({
|
||||||
|
id,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key.id,
|
||||||
|
isActive: !key.isDisabled
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.findOne({
|
||||||
|
id: clientId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipClient.permissions?.includes(KmipPermission.Revoke)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client does not have sufficient permission to perform KMIP revoke"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await kmsDAL.findOne({
|
||||||
|
id,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.isReserved) {
|
||||||
|
throw new BadRequestError({ message: "Cannot revoke reserved keys" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
|
||||||
|
|
||||||
|
if (!completeKeyDetails.internalKms) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot revoke external keys"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const revokedKey = await kmsDAL.updateById(key.id, {
|
||||||
|
isDisabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key.id,
|
||||||
|
updatedAt: revokedKey.updatedAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAttributes = async ({
|
||||||
|
projectId,
|
||||||
|
id,
|
||||||
|
clientId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TKmipGetAttributesDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.findOne({
|
||||||
|
id: clientId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipClient.permissions?.includes(KmipPermission.GetAttributes)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client does not have sufficient permission to perform KMIP get attributes"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await kmsDAL.findOne({
|
||||||
|
id,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.isReserved) {
|
||||||
|
throw new BadRequestError({ message: "Cannot get reserved keys" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
|
||||||
|
|
||||||
|
if (!completeKeyDetails.internalKms) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot get external keys"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key.id,
|
||||||
|
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
||||||
|
isActive: !key.isDisabled,
|
||||||
|
createdAt: key.createdAt,
|
||||||
|
updatedAt: key.updatedAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.findOne({
|
||||||
|
id: clientId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipClient.permissions?.includes(KmipPermission.Locate)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client does not have sufficient permission to perform KMIP locate"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = await kmsDAL.findProjectCmeks(projectId);
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async ({
|
||||||
|
projectId,
|
||||||
|
clientId,
|
||||||
|
key,
|
||||||
|
algorithm,
|
||||||
|
name,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TKmipRegisterDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.findOne({
|
||||||
|
id: clientId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipClient.permissions?.includes(KmipPermission.Register)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Client does not have sufficient permission to perform KMIP register"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
|
||||||
|
const kmsKey = await kmsService.importKeyMaterial({
|
||||||
|
name,
|
||||||
|
key: Buffer.from(key, "base64"),
|
||||||
|
algorithm,
|
||||||
|
isReserved: false,
|
||||||
|
projectId,
|
||||||
|
orgId: project.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmsKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
activate,
|
||||||
|
getAttributes,
|
||||||
|
destroy,
|
||||||
|
revoke,
|
||||||
|
locate,
|
||||||
|
register
|
||||||
|
};
|
||||||
|
};
|
11
backend/src/ee/services/kmip/kmip-org-config-dal.ts
Normal file
11
backend/src/ee/services/kmip/kmip-org-config-dal.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TKmipOrgConfigDALFactory = ReturnType<typeof kmipOrgConfigDALFactory>;
|
||||||
|
|
||||||
|
export const kmipOrgConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const kmipOrgConfigOrm = ormify(db, TableName.KmipOrgConfig);
|
||||||
|
|
||||||
|
return kmipOrgConfigOrm;
|
||||||
|
};
|
@@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TKmipOrgServerCertificateDALFactory = ReturnType<typeof kmipOrgServerCertificateDALFactory>;
|
||||||
|
|
||||||
|
export const kmipOrgServerCertificateDALFactory = (db: TDbClient) => {
|
||||||
|
const kmipOrgServerCertificateOrm = ormify(db, TableName.KmipOrgServerCertificates);
|
||||||
|
|
||||||
|
return kmipOrgServerCertificateOrm;
|
||||||
|
};
|
817
backend/src/ee/services/kmip/kmip-service.ts
Normal file
817
backend/src/ee/services/kmip/kmip-service.ts
Normal file
@@ -0,0 +1,817 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import crypto, { KeyObject } from "crypto";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
||||||
|
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||||
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
|
import {
|
||||||
|
createSerialNumber,
|
||||||
|
keyAlgorithmToAlgCfg
|
||||||
|
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
|
import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal";
|
||||||
|
import { TKmipClientDALFactory } from "./kmip-client-dal";
|
||||||
|
import { TKmipOrgConfigDALFactory } from "./kmip-org-config-dal";
|
||||||
|
import { TKmipOrgServerCertificateDALFactory } from "./kmip-org-server-certificate-dal";
|
||||||
|
import {
|
||||||
|
TCreateKmipClientCertificateDTO,
|
||||||
|
TCreateKmipClientDTO,
|
||||||
|
TDeleteKmipClientDTO,
|
||||||
|
TGenerateOrgKmipServerCertificateDTO,
|
||||||
|
TGetKmipClientDTO,
|
||||||
|
TGetOrgKmipDTO,
|
||||||
|
TListKmipClientsByProjectIdDTO,
|
||||||
|
TRegisterServerDTO,
|
||||||
|
TSetupOrgKmipDTO,
|
||||||
|
TUpdateKmipClientDTO
|
||||||
|
} from "./kmip-types";
|
||||||
|
|
||||||
|
type TKmipServiceFactoryDep = {
|
||||||
|
kmipClientDAL: TKmipClientDALFactory;
|
||||||
|
kmipClientCertificateDAL: TKmipClientCertificateDALFactory;
|
||||||
|
kmipOrgServerCertificateDAL: TKmipOrgServerCertificateDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
kmipOrgConfigDAL: TKmipOrgConfigDALFactory;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>;
|
||||||
|
|
||||||
|
export const kmipServiceFactory = ({
|
||||||
|
kmipClientDAL,
|
||||||
|
permissionService,
|
||||||
|
kmipClientCertificateDAL,
|
||||||
|
kmipOrgConfigDAL,
|
||||||
|
kmsService,
|
||||||
|
kmipOrgServerCertificateDAL,
|
||||||
|
licenseService
|
||||||
|
}: TKmipServiceFactoryDep) => {
|
||||||
|
const createKmipClient = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
permissions
|
||||||
|
}: TCreateKmipClientDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionKmipActions.CreateClients,
|
||||||
|
ProjectPermissionSub.Kmip
|
||||||
|
);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.kmip)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create KMIP client. Upgrade your plan to enterprise."
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmipClient = await kmipClientDAL.create({
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
permissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmipClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateKmipClient = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
permissions,
|
||||||
|
id
|
||||||
|
}: TUpdateKmipClientDTO) => {
|
||||||
|
const kmipClient = await kmipClientDAL.findById(id);
|
||||||
|
|
||||||
|
if (!kmipClient) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `KMIP client with ID ${id} does not exist`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.kmip)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to update KMIP client. Upgrade your plan to enterprise."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: kmipClient.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionKmipActions.UpdateClients,
|
||||||
|
ProjectPermissionSub.Kmip
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedKmipClient = await kmipClientDAL.updateById(id, {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
permissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedKmipClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => {
|
||||||
|
const kmipClient = await kmipClientDAL.findById(id);
|
||||||
|
|
||||||
|
if (!kmipClient) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `KMIP client with ID ${id} does not exist`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: kmipClient.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionKmipActions.DeleteClients,
|
||||||
|
ProjectPermissionSub.Kmip
|
||||||
|
);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.kmip)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to delete KMIP client. Upgrade your plan to enterprise."
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletedKmipClient = await kmipClientDAL.deleteById(id);
|
||||||
|
|
||||||
|
return deletedKmipClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => {
|
||||||
|
const kmipClient = await kmipClientDAL.findById(id);
|
||||||
|
|
||||||
|
if (!kmipClient) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `KMIP client with ID ${id} does not exist`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: kmipClient.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
|
||||||
|
|
||||||
|
return kmipClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listKmipClientsByProjectId = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
...rest
|
||||||
|
}: TListKmipClientsByProjectIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
|
||||||
|
|
||||||
|
return kmipClientDAL.findByProjectId({ projectId, ...rest });
|
||||||
|
};
|
||||||
|
|
||||||
|
const createKmipClientCertificate = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
ttl,
|
||||||
|
keyAlgorithm,
|
||||||
|
clientId
|
||||||
|
}: TCreateKmipClientCertificateDTO) => {
|
||||||
|
const kmipClient = await kmipClientDAL.findById(clientId);
|
||||||
|
|
||||||
|
if (!kmipClient) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `KMIP client with ID ${clientId} does not exist`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.kmip)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create KMIP client. Upgrade your plan to enterprise."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: kmipClient.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionKmipActions.GenerateClientCertificates,
|
||||||
|
ProjectPermissionSub.Kmip
|
||||||
|
);
|
||||||
|
|
||||||
|
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipConfig) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "KMIP has not been configured for the organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(
|
||||||
|
decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate })
|
||||||
|
);
|
||||||
|
|
||||||
|
const notBeforeDate = new Date();
|
||||||
|
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||||
|
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
|
||||||
|
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const extensions: x509.Extension[] = [
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
|
||||||
|
];
|
||||||
|
|
||||||
|
const caAlg = keyAlgorithmToAlgCfg(kmipConfig.caKeyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const caSkObj = crypto.createPrivateKey({
|
||||||
|
key: decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaPrivateKey }),
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
|
||||||
|
const caPrivateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
caSkObj.export({ format: "der", type: "pkcs8" }),
|
||||||
|
caAlg,
|
||||||
|
true,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const serialNumber = createSerialNumber();
|
||||||
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: `OU=${kmipClient.projectId},CN=${clientId}`,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
publicKey: leafKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions
|
||||||
|
});
|
||||||
|
|
||||||
|
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||||
|
|
||||||
|
const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate }));
|
||||||
|
const serverIntermediateCaCert = new x509.X509Certificate(
|
||||||
|
decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate })
|
||||||
|
);
|
||||||
|
|
||||||
|
await kmipClientCertificateDAL.create({
|
||||||
|
kmipClientId: clientId,
|
||||||
|
keyAlgorithm,
|
||||||
|
issuedAt: notBeforeDate,
|
||||||
|
expiration: notAfterDate,
|
||||||
|
serialNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
|
||||||
|
certificate: leafCert.toString("pem"),
|
||||||
|
certificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
|
||||||
|
projectId: kmipClient.projectId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerCertificateBySerialNumber = async (orgId: string, serialNumber: string) => {
|
||||||
|
const serverCert = await kmipOrgServerCertificateDAL.findOne({
|
||||||
|
serialNumber,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!serverCert) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Server certificate not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedCertificate = new x509.X509Certificate(decryptor({ cipherTextBlob: serverCert.encryptedCertificate }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: parsedCertificate.publicKey.toString("pem"),
|
||||||
|
keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (kmipConfig) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "KMIP has already been configured for the organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.kmip)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to setup KMIP. Upgrade your plan to enterprise."
|
||||||
|
});
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm);
|
||||||
|
|
||||||
|
// generate root CA
|
||||||
|
const rootCaSerialNumber = createSerialNumber();
|
||||||
|
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey);
|
||||||
|
const rootCaIssuedAt = new Date();
|
||||||
|
const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20));
|
||||||
|
|
||||||
|
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||||
|
name: `CN=KMIP Root CA,OU=${actorOrgId}`,
|
||||||
|
serialNumber: rootCaSerialNumber,
|
||||||
|
notBefore: rootCaIssuedAt,
|
||||||
|
notAfter: rootCaExpiration,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
keys: rootCaKeys,
|
||||||
|
extensions: [
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate intermediate server CA
|
||||||
|
const serverIntermediateCaSerialNumber = createSerialNumber();
|
||||||
|
const serverIntermediateCaIssuedAt = new Date();
|
||||||
|
const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||||
|
const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey);
|
||||||
|
|
||||||
|
const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber: serverIntermediateCaSerialNumber,
|
||||||
|
subject: `CN=KMIP Server Intermediate CA,OU=${actorOrgId}`,
|
||||||
|
issuer: rootCaCert.subject,
|
||||||
|
notBefore: serverIntermediateCaIssuedAt,
|
||||||
|
notAfter: serverIntermediateCaExpiration,
|
||||||
|
signingKey: rootCaKeys.privateKey,
|
||||||
|
publicKey: serverIntermediateCaKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags.keyCertSign |
|
||||||
|
x509.KeyUsageFlags.cRLSign |
|
||||||
|
x509.KeyUsageFlags.digitalSignature |
|
||||||
|
x509.KeyUsageFlags.keyEncipherment,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.BasicConstraintsExtension(true, 0, true),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate intermediate client CA
|
||||||
|
const clientIntermediateCaSerialNumber = createSerialNumber();
|
||||||
|
const clientIntermediateCaIssuedAt = new Date();
|
||||||
|
const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||||
|
const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey);
|
||||||
|
|
||||||
|
const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber: clientIntermediateCaSerialNumber,
|
||||||
|
subject: `CN=KMIP Client Intermediate CA,OU=${actorOrgId}`,
|
||||||
|
issuer: rootCaCert.subject,
|
||||||
|
notBefore: clientIntermediateCaIssuedAt,
|
||||||
|
notAfter: clientIntermediateCaExpiration,
|
||||||
|
signingKey: rootCaKeys.privateKey,
|
||||||
|
publicKey: clientIntermediateCaKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags.keyCertSign |
|
||||||
|
x509.KeyUsageFlags.cRLSign |
|
||||||
|
x509.KeyUsageFlags.digitalSignature |
|
||||||
|
x509.KeyUsageFlags.keyEncipherment,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.BasicConstraintsExtension(true, 0, true),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await kmipOrgConfigDAL.create({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
caKeyAlgorithm,
|
||||||
|
rootCaIssuedAt,
|
||||||
|
rootCaExpiration,
|
||||||
|
rootCaSerialNumber,
|
||||||
|
encryptedRootCaCertificate: encryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
|
||||||
|
encryptedRootCaPrivateKey: encryptor({
|
||||||
|
plainText: rootCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
serverIntermediateCaIssuedAt,
|
||||||
|
serverIntermediateCaExpiration,
|
||||||
|
serverIntermediateCaSerialNumber,
|
||||||
|
encryptedServerIntermediateCaCertificate: encryptor({
|
||||||
|
plainText: Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData))
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedServerIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) })
|
||||||
|
.cipherTextBlob,
|
||||||
|
encryptedServerIntermediateCaPrivateKey: encryptor({
|
||||||
|
plainText: serverIntermediateCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
clientIntermediateCaIssuedAt,
|
||||||
|
clientIntermediateCaExpiration,
|
||||||
|
clientIntermediateCaSerialNumber,
|
||||||
|
encryptedClientIntermediateCaCertificate: encryptor({
|
||||||
|
plainText: Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData))
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedClientIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) })
|
||||||
|
.cipherTextBlob,
|
||||||
|
encryptedClientIntermediateCaPrivateKey: encryptor({
|
||||||
|
plainText: clientIntermediateCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
|
||||||
|
clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert])
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => {
|
||||||
|
await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId);
|
||||||
|
|
||||||
|
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipConfig) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "KMIP has not been configured for the organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate }));
|
||||||
|
const serverIntermediateCaCert = new x509.X509Certificate(
|
||||||
|
decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate })
|
||||||
|
);
|
||||||
|
|
||||||
|
const clientIntermediateCaCert = new x509.X509Certificate(
|
||||||
|
decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate })
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: kmipConfig.id,
|
||||||
|
serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
|
||||||
|
clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert])
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateOrgKmipServerCertificate = async ({
|
||||||
|
orgId,
|
||||||
|
ttl,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
keyAlgorithm
|
||||||
|
}: TGenerateOrgKmipServerCertificateDTO) => {
|
||||||
|
const kmipOrgConfig = await kmipOrgConfigDAL.findOne({
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipOrgConfig) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "KMIP has not been configured for the organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.kmip)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to generate KMIP server certificate. Upgrade your plan to enterprise."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(
|
||||||
|
decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaCertificate })
|
||||||
|
);
|
||||||
|
|
||||||
|
const notBeforeDate = new Date();
|
||||||
|
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||||
|
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
|
||||||
|
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const extensions: x509.Extension[] = [
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true)
|
||||||
|
];
|
||||||
|
|
||||||
|
const altNamesArray: {
|
||||||
|
type: "email" | "dns" | "ip";
|
||||||
|
value: string;
|
||||||
|
}[] = altNames
|
||||||
|
.split(",")
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.map((altName) => {
|
||||||
|
if (isValidHostname(altName)) {
|
||||||
|
return {
|
||||||
|
type: "dns",
|
||||||
|
value: altName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidIp(altName)) {
|
||||||
|
return {
|
||||||
|
type: "ip",
|
||||||
|
value: altName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid altName: ${altName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
|
extensions.push(altNamesExtension);
|
||||||
|
|
||||||
|
const caAlg = keyAlgorithmToAlgCfg(kmipOrgConfig.caKeyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const decryptedCaCertChain = decryptor({
|
||||||
|
cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaChain
|
||||||
|
}).toString("utf-8");
|
||||||
|
|
||||||
|
const caSkObj = crypto.createPrivateKey({
|
||||||
|
key: decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaPrivateKey }),
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
|
||||||
|
const caPrivateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
caSkObj.export({ format: "der", type: "pkcs8" }),
|
||||||
|
caAlg,
|
||||||
|
true,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const serialNumber = createSerialNumber();
|
||||||
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: `CN=${commonName}`,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
publicKey: leafKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions
|
||||||
|
});
|
||||||
|
|
||||||
|
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||||
|
const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim();
|
||||||
|
|
||||||
|
await kmipOrgServerCertificateDAL.create({
|
||||||
|
orgId,
|
||||||
|
keyAlgorithm,
|
||||||
|
issuedAt: notBeforeDate,
|
||||||
|
expiration: notAfterDate,
|
||||||
|
serialNumber,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
encryptedCertificate: encryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }).cipherTextBlob,
|
||||||
|
encryptedChain: encryptor({ plainText: Buffer.from(certificateChain) }).cipherTextBlob
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
|
||||||
|
certificate: leafCert.toString("pem"),
|
||||||
|
certificateChain
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerServer = async ({
|
||||||
|
actorOrgId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
ttl,
|
||||||
|
commonName,
|
||||||
|
keyAlgorithm,
|
||||||
|
hostnamesOrIps
|
||||||
|
}: TRegisterServerDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kmipConfig) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "KMIP has not been configured for the organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.kmip)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to register KMIP server. Upgrade your plan to enterprise."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { privateKey, certificate, certificateChain, serialNumber } = await generateOrgKmipServerCertificate({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
commonName: commonName ?? "kmip-server",
|
||||||
|
altNames: hostnamesOrIps,
|
||||||
|
keyAlgorithm: keyAlgorithm ?? (kmipConfig.caKeyAlgorithm as CertKeyAlgorithm),
|
||||||
|
ttl
|
||||||
|
});
|
||||||
|
|
||||||
|
const { clientCertificateChain } = await getOrgKmip({
|
||||||
|
actor,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorId,
|
||||||
|
actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverCertificateSerialNumber: serialNumber,
|
||||||
|
clientCertificateChain,
|
||||||
|
privateKey,
|
||||||
|
certificate,
|
||||||
|
certificateChain
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createKmipClient,
|
||||||
|
updateKmipClient,
|
||||||
|
deleteKmipClient,
|
||||||
|
getKmipClient,
|
||||||
|
listKmipClientsByProjectId,
|
||||||
|
createKmipClientCertificate,
|
||||||
|
setupOrgKmip,
|
||||||
|
generateOrgKmipServerCertificate,
|
||||||
|
getOrgKmip,
|
||||||
|
getServerCertificateBySerialNumber,
|
||||||
|
registerServer
|
||||||
|
};
|
||||||
|
};
|
102
backend/src/ee/services/kmip/kmip-types.ts
Normal file
102
backend/src/ee/services/kmip/kmip-types.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
|
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
import { KmipPermission } from "./kmip-enum";
|
||||||
|
|
||||||
|
export type TCreateKmipClientCertificateDTO = {
|
||||||
|
clientId: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
ttl: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TCreateKmipClientDTO = {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
permissions: KmipPermission[];
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateKmipClientDTO = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
permissions?: KmipPermission[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteKmipClientDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetKmipClientDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export enum KmipClientOrderBy {
|
||||||
|
Name = "name"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TListKmipClientsByProjectIdDTO = {
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: KmipClientOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
type KmipOperationBaseDTO = {
|
||||||
|
clientId: string;
|
||||||
|
projectId: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TKmipCreateDTO = {
|
||||||
|
algorithm: SymmetricEncryption;
|
||||||
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
|
export type TKmipGetDTO = {
|
||||||
|
id: string;
|
||||||
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
|
export type TKmipGetAttributesDTO = {
|
||||||
|
id: string;
|
||||||
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
|
export type TKmipDestroyDTO = {
|
||||||
|
id: string;
|
||||||
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
|
export type TKmipActivateDTO = {
|
||||||
|
id: string;
|
||||||
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
|
export type TKmipRevokeDTO = {
|
||||||
|
id: string;
|
||||||
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
|
export type TKmipLocateDTO = KmipOperationBaseDTO;
|
||||||
|
|
||||||
|
export type TKmipRegisterDTO = {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
algorithm: SymmetricEncryption;
|
||||||
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
|
export type TSetupOrgKmipDTO = {
|
||||||
|
caKeyAlgorithm: CertKeyAlgorithm;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetOrgKmipDTO = Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGenerateOrgKmipServerCertificateDTO = {
|
||||||
|
commonName: string;
|
||||||
|
altNames: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
ttl: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRegisterServerDTO = {
|
||||||
|
hostnamesOrIps: string;
|
||||||
|
commonName?: string;
|
||||||
|
keyAlgorithm?: CertKeyAlgorithm;
|
||||||
|
ttl: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
@@ -50,7 +50,9 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
},
|
},
|
||||||
pkiEst: false,
|
pkiEst: false,
|
||||||
enforceMfa: false,
|
enforceMfa: false,
|
||||||
projectTemplates: false
|
projectTemplates: false,
|
||||||
|
kmip: false,
|
||||||
|
gateway: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@@ -68,6 +68,8 @@ export type TFeatureSet = {
|
|||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
projectTemplates: false;
|
projectTemplates: false;
|
||||||
|
kmip: false;
|
||||||
|
gateway: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@@ -23,10 +23,23 @@ export enum OrgPermissionAppConnectionActions {
|
|||||||
Connect = "connect"
|
Connect = "connect"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionKmipActions {
|
||||||
|
Proxy = "proxy",
|
||||||
|
Setup = "setup"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionAdminConsoleAction {
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
AccessAllProjects = "access-all-projects"
|
AccessAllProjects = "access-all-projects"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionGatewayActions {
|
||||||
|
// is there a better word for this. This mean can an identity be a gateway
|
||||||
|
CreateGateways = "create-gateways",
|
||||||
|
ListGateways = "list-gateways",
|
||||||
|
EditGateways = "edit-gateways",
|
||||||
|
DeleteGateways = "delete-gateways"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionSubjects {
|
export enum OrgPermissionSubjects {
|
||||||
Workspace = "workspace",
|
Workspace = "workspace",
|
||||||
Role = "role",
|
Role = "role",
|
||||||
@@ -44,7 +57,9 @@ export enum OrgPermissionSubjects {
|
|||||||
AdminConsole = "organization-admin-console",
|
AdminConsole = "organization-admin-console",
|
||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
ProjectTemplates = "project-templates",
|
ProjectTemplates = "project-templates",
|
||||||
AppConnections = "app-connections"
|
AppConnections = "app-connections",
|
||||||
|
Kmip = "kmip",
|
||||||
|
Gateway = "gateway"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppConnectionSubjectFields = {
|
export type AppConnectionSubjectFields = {
|
||||||
@@ -67,6 +82,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
|
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||||
| [
|
| [
|
||||||
OrgPermissionAppConnectionActions,
|
OrgPermissionAppConnectionActions,
|
||||||
(
|
(
|
||||||
@@ -74,7 +90,8 @@ export type OrgPermissionSet =
|
|||||||
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
|
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||||
|
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
|
||||||
|
|
||||||
const AppConnectionConditionSchema = z
|
const AppConnectionConditionSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -167,6 +184,18 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
|||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -251,8 +280,18 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
|
can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
|
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
|
||||||
|
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -282,6 +321,8 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
@@ -44,6 +44,14 @@ export enum ProjectPermissionSecretSyncActions {
|
|||||||
RemoveSecrets = "remove-secrets"
|
RemoveSecrets = "remove-secrets"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionKmipActions {
|
||||||
|
CreateClients = "create-clients",
|
||||||
|
UpdateClients = "update-clients",
|
||||||
|
DeleteClients = "delete-clients",
|
||||||
|
ReadClients = "read-clients",
|
||||||
|
GenerateClientCertificates = "generate-client-certificates"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
@@ -75,7 +83,8 @@ export enum ProjectPermissionSub {
|
|||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
Cmek = "cmek",
|
Cmek = "cmek",
|
||||||
SecretSyncs = "secret-syncs"
|
SecretSyncs = "secret-syncs",
|
||||||
|
Kmip = "kmip"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SecretSubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
@@ -156,6 +165,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||||
|
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
|
||||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||||
@@ -410,6 +420,12 @@ const GeneralPermissionSchema = [
|
|||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -575,6 +591,18 @@ const buildAdminPermissionRules = () => {
|
|||||||
],
|
],
|
||||||
ProjectPermissionSub.SecretSyncs
|
ProjectPermissionSub.SecretSyncs
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionKmipActions.CreateClients,
|
||||||
|
ProjectPermissionKmipActions.UpdateClients,
|
||||||
|
ProjectPermissionKmipActions.DeleteClients,
|
||||||
|
ProjectPermissionKmipActions.ReadClients,
|
||||||
|
ProjectPermissionKmipActions.GenerateClientCertificates
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Kmip
|
||||||
|
);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -100,6 +100,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
||||||
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
||||||
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||||
|
tx.ref("comment").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerComment"),
|
||||||
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
||||||
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
||||||
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
||||||
@@ -162,8 +163,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
reviewerEmail: email,
|
reviewerEmail: email,
|
||||||
reviewerLastName: lastName,
|
reviewerLastName: lastName,
|
||||||
reviewerUsername: username,
|
reviewerUsername: username,
|
||||||
reviewerFirstName: firstName
|
reviewerFirstName: firstName,
|
||||||
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined)
|
reviewerComment: comment
|
||||||
|
}) =>
|
||||||
|
userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
|
@@ -320,6 +320,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
approvalId,
|
approvalId,
|
||||||
actor,
|
actor,
|
||||||
status,
|
status,
|
||||||
|
comment,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
@@ -372,15 +373,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
return secretApprovalRequestReviewerDAL.create(
|
return secretApprovalRequestReviewerDAL.create(
|
||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
|
comment,
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
reviewerUserId: actorId
|
reviewerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return secretApprovalRequestReviewerDAL.updateById(review.id, { status }, tx);
|
|
||||||
|
return secretApprovalRequestReviewerDAL.updateById(review.id, { status, comment }, tx);
|
||||||
});
|
});
|
||||||
return reviewStatus;
|
|
||||||
|
return { ...reviewStatus, projectId: secretApprovalRequest.projectId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateApprovalStatus = async ({
|
const updateApprovalStatus = async ({
|
||||||
@@ -1294,7 +1298,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretMetadata
|
secretMetadata
|
||||||
}) => {
|
}) => {
|
||||||
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
||||||
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
|
if (tagIds?.length) commitTagIds[newSecretName ?? secretKey] = tagIds;
|
||||||
return {
|
return {
|
||||||
...latestSecretVersions[secretId],
|
...latestSecretVersions[secretId],
|
||||||
secretMetadata,
|
secretMetadata,
|
||||||
|
@@ -80,6 +80,7 @@ export type TStatusChangeDTO = {
|
|||||||
export type TReviewRequestDTO = {
|
export type TReviewRequestDTO = {
|
||||||
approvalId: string;
|
approvalId: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
comment?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TApprovalRequestCountDTO = TProjectPermission;
|
export type TApprovalRequestCountDTO = TProjectPermission;
|
||||||
|
@@ -13,6 +13,7 @@ import { NotFoundError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -332,6 +333,7 @@ export const secretRotationQueueFactory = ({
|
|||||||
await secretVersionV2BridgeDAL.insertMany(
|
await secretVersionV2BridgeDAL.insertMany(
|
||||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
|
actorType: ActorType.PLATFORM,
|
||||||
secretId: id
|
secretId: id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
|
@@ -7,6 +7,7 @@ import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
|||||||
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -370,7 +371,21 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
const secrets = await secretV2BridgeDAL.insertMany(
|
const secrets = await secretV2BridgeDAL.insertMany(
|
||||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||||
secretVersions.map(
|
secretVersions.map(
|
||||||
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
|
({
|
||||||
|
latestSecretVersion,
|
||||||
|
version,
|
||||||
|
updatedAt,
|
||||||
|
createdAt,
|
||||||
|
secretId,
|
||||||
|
envId,
|
||||||
|
id,
|
||||||
|
tags,
|
||||||
|
// exclude the bottom fields from the secret - they are for versioning only.
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType,
|
||||||
|
...el
|
||||||
|
}) => ({
|
||||||
...el,
|
...el,
|
||||||
id: secretId,
|
id: secretId,
|
||||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||||
@@ -401,8 +416,18 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
const userActorId = actor === ActorType.USER ? actorId : undefined;
|
||||||
|
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
|
||||||
|
const actorType = actor || ActorType.PLATFORM;
|
||||||
|
|
||||||
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
||||||
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })),
|
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
secretId: id,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType
|
||||||
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await secretVersionV2TagBridgeDAL.insertMany(
|
await secretVersionV2TagBridgeDAL.insertMany(
|
||||||
|
@@ -6,7 +6,6 @@ export const sanitizedSshCertificate = SshCertificatesSchema.pick({
|
|||||||
sshCertificateTemplateId: true,
|
sshCertificateTemplateId: true,
|
||||||
serialNumber: true,
|
serialNumber: true,
|
||||||
certType: true,
|
certType: true,
|
||||||
publicKey: true,
|
|
||||||
principals: true,
|
principals: true,
|
||||||
keyId: true,
|
keyId: true,
|
||||||
notBefore: true,
|
notBefore: true,
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
|
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||||
import { Redlock, Settings } from "@app/lib/red-lock";
|
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||||
|
|
||||||
export enum PgSqlLock {
|
export const PgSqlLock = {
|
||||||
BootUpMigration = 2023,
|
BootUpMigration: 2023,
|
||||||
SuperAdminInit = 2024,
|
SuperAdminInit: 2024,
|
||||||
KmsRootKeyInit = 2025
|
KmsRootKeyInit: 2025,
|
||||||
}
|
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
||||||
|
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`)
|
||||||
|
} as const;
|
||||||
|
|
||||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||||
|
|
||||||
@@ -33,7 +36,8 @@ export const KeyStorePrefixes = {
|
|||||||
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||||
`identity-access-token-status:${identityAccessTokenId}`,
|
`identity-access-token-status:${identityAccessTokenId}`,
|
||||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
|
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
|
||||||
|
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KeyStoreTtls = {
|
export const KeyStoreTtls = {
|
||||||
|
@@ -638,7 +638,8 @@ export const FOLDERS = {
|
|||||||
environment: "The slug of the environment to create the folder in.",
|
environment: "The slug of the environment to create the folder in.",
|
||||||
name: "The name of the folder to create.",
|
name: "The name of the folder to create.",
|
||||||
path: "The path of the folder to create.",
|
path: "The path of the folder to create.",
|
||||||
directory: "The directory of the folder to create. (Deprecated in favor of path)"
|
directory: "The directory of the folder to create. (Deprecated in favor of path)",
|
||||||
|
description: "An optional description label for the folder."
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
folderId: "The ID of the folder to update.",
|
folderId: "The ID of the folder to update.",
|
||||||
@@ -647,7 +648,8 @@ export const FOLDERS = {
|
|||||||
path: "The path of the folder to update.",
|
path: "The path of the folder to update.",
|
||||||
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
||||||
projectSlug: "The slug of the project where the folder is located.",
|
projectSlug: "The slug of the project where the folder is located.",
|
||||||
workspaceId: "The ID of the project where the folder is located."
|
workspaceId: "The ID of the project where the folder is located.",
|
||||||
|
description: "An optional description label for the folder."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
folderIdOrName: "The ID or name of the folder to delete.",
|
folderIdOrName: "The ID or name of the folder to delete.",
|
||||||
@@ -721,7 +723,8 @@ export const RAW_SECRETS = {
|
|||||||
secretName: "The name of the secret to update.",
|
secretName: "The name of the secret to update.",
|
||||||
secretComment: "Update comment to the secret.",
|
secretComment: "Update comment to the secret.",
|
||||||
environment: "The slug of the environment where the secret is located.",
|
environment: "The slug of the environment where the secret is located.",
|
||||||
secretPath: "The path of the secret to update.",
|
mode: "Defines how the system should handle missing secrets during an update.",
|
||||||
|
secretPath: "The default path for secrets to update or upsert, if not provided in the secret details.",
|
||||||
secretValue: "The new value of the secret.",
|
secretValue: "The new value of the secret.",
|
||||||
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
||||||
type: "The type of the secret to update.",
|
type: "The type of the secret to update.",
|
||||||
@@ -1721,6 +1724,18 @@ export const SecretSyncs = {
|
|||||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
ADDITIONAL_SYNC_OPTIONS: {
|
||||||
|
AWS_PARAMETER_STORE: {
|
||||||
|
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||||
|
tags: "Optional resource tags to add to parameters synced by Infisical.",
|
||||||
|
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as resource tags to parameters synced by Infisical.`
|
||||||
|
},
|
||||||
|
AWS_SECRETS_MANAGER: {
|
||||||
|
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||||
|
tags: "Optional tags to add to secrets synced by Infisical.",
|
||||||
|
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
|
||||||
|
}
|
||||||
|
},
|
||||||
DESTINATION_CONFIG: {
|
DESTINATION_CONFIG: {
|
||||||
AWS_PARAMETER_STORE: {
|
AWS_PARAMETER_STORE: {
|
||||||
region: "The AWS region to sync secrets to.",
|
region: "The AWS region to sync secrets to.",
|
||||||
|
@@ -24,6 +24,7 @@ const databaseReadReplicaSchema = z
|
|||||||
|
|
||||||
const envSchema = z
|
const envSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
INFISICAL_PLATFORM_VERSION: zpStr(z.string().optional()),
|
||||||
PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000),
|
PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000),
|
||||||
DISABLE_SECRET_SCANNING: z
|
DISABLE_SECRET_SCANNING: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
@@ -184,6 +185,14 @@ const envSchema = z
|
|||||||
USE_PG_QUEUE: zodStrBool.default("false"),
|
USE_PG_QUEUE: zodStrBool.default("false"),
|
||||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
||||||
|
|
||||||
|
/* Gateway----------------------------------------------------------------------------- */
|
||||||
|
GATEWAY_INFISICAL_STATIC_IP_ADDRESS: zpStr(z.string().optional()),
|
||||||
|
GATEWAY_RELAY_ADDRESS: zpStr(z.string().optional()),
|
||||||
|
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
|
||||||
|
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* App Connections ----------------------------------------------------------------------------- */
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
// aws
|
// aws
|
||||||
@@ -208,6 +217,13 @@ const envSchema = z
|
|||||||
INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// datadog
|
||||||
|
SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"),
|
||||||
|
DATADOG_PROFILING_ENABLED: zodStrBool.default("false"),
|
||||||
|
DATADOG_ENV: zpStr(z.string().optional().default("prod")),
|
||||||
|
DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")),
|
||||||
|
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
||||||
|
|
||||||
/* CORS ----------------------------------------------------------------------------- */
|
/* CORS ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS: zpStr(
|
CORS_ALLOWED_ORIGINS: zpStr(
|
||||||
|
29
backend/src/lib/crypto/hashtext.ts
Normal file
29
backend/src/lib/crypto/hashtext.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// used for postgres lock
|
||||||
|
// this is something postgres does under the hood
|
||||||
|
// convert any string to a unique number
|
||||||
|
export const hashtext = (text: string) => {
|
||||||
|
// Convert text to UTF8 bytes array for consistent behavior with PostgreSQL
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const bytes = encoder.encode(text);
|
||||||
|
|
||||||
|
// Implementation of hash_any
|
||||||
|
let result = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < bytes.length; i += 1) {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
result = ((result << 5) + result) ^ bytes[i];
|
||||||
|
// Keep within 32-bit integer range
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
result >>>= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to signed 32-bit integer like PostgreSQL
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
return result | 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pgAdvisoryLockHashText = (text: string) => {
|
||||||
|
const hash = hashtext(text);
|
||||||
|
// Ensure positive value within PostgreSQL integer range
|
||||||
|
return Math.abs(hash) % 2 ** 31;
|
||||||
|
};
|
339
backend/src/lib/gateway/index.ts
Normal file
339
backend/src/lib/gateway/index.ts
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import net from "node:net";
|
||||||
|
|
||||||
|
import quicDefault, * as quicModule from "@infisical/quic";
|
||||||
|
|
||||||
|
import { BadRequestError } from "../errors";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
const DEFAULT_MAX_RETRIES = 3;
|
||||||
|
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
||||||
|
|
||||||
|
const quic = quicDefault || quicModule;
|
||||||
|
|
||||||
|
const parseSubjectDetails = (data: string) => {
|
||||||
|
const values: Record<string, string> = {};
|
||||||
|
data.split("\n").forEach((el) => {
|
||||||
|
const [key, value] = el.split("=");
|
||||||
|
values[key.trim()] = value.trim();
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TTlsOption = { ca: string; cert: string; key: string };
|
||||||
|
|
||||||
|
const createQuicConnection = async (
|
||||||
|
relayHost: string,
|
||||||
|
relayPort: number,
|
||||||
|
tlsOptions: TTlsOption,
|
||||||
|
identityId: string,
|
||||||
|
orgId: string
|
||||||
|
) => {
|
||||||
|
const client = await quic.QUICClient.createQUICClient({
|
||||||
|
host: relayHost,
|
||||||
|
port: relayPort,
|
||||||
|
config: {
|
||||||
|
ca: tlsOptions.ca,
|
||||||
|
cert: tlsOptions.cert,
|
||||||
|
key: tlsOptions.key,
|
||||||
|
applicationProtos: ["infisical-gateway"],
|
||||||
|
verifyPeer: true,
|
||||||
|
verifyCallback: async (certs) => {
|
||||||
|
if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired;
|
||||||
|
const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0]));
|
||||||
|
const caCertificate = new crypto.X509Certificate(tlsOptions.ca);
|
||||||
|
const isValidServerCertificate = serverCertificate.checkIssued(caCertificate);
|
||||||
|
if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate;
|
||||||
|
|
||||||
|
const subjectDetails = parseSubjectDetails(serverCertificate.subject);
|
||||||
|
if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) {
|
||||||
|
return quic.native.CryptoError.CertificateUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) {
|
||||||
|
return quic.native.CryptoError.CertificateExpired;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatedRelayHost =
|
||||||
|
process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost;
|
||||||
|
if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate;
|
||||||
|
},
|
||||||
|
maxIdleTimeout: 90000,
|
||||||
|
keepAliveIntervalTime: 30000
|
||||||
|
},
|
||||||
|
crypto: {
|
||||||
|
ops: {
|
||||||
|
randomBytes: async (data) => {
|
||||||
|
crypto.getRandomValues(new Uint8Array(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TPingGatewayAndVerifyDTO = {
|
||||||
|
relayHost: string;
|
||||||
|
relayPort: number;
|
||||||
|
tlsOptions: TTlsOption;
|
||||||
|
maxRetries?: number;
|
||||||
|
identityId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pingGatewayAndVerify = async ({
|
||||||
|
relayHost,
|
||||||
|
relayPort,
|
||||||
|
tlsOptions,
|
||||||
|
maxRetries = DEFAULT_MAX_RETRIES,
|
||||||
|
identityId,
|
||||||
|
orgId
|
||||||
|
}: TPingGatewayAndVerifyDTO) => {
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||||
|
throw new BadRequestError({
|
||||||
|
error: err as Error
|
||||||
|
});
|
||||||
|
});
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
||||||
|
try {
|
||||||
|
const stream = quicClient.connection.newStream("bidi");
|
||||||
|
const pingWriter = stream.writable.getWriter();
|
||||||
|
await pingWriter.write(Buffer.from("PING\n"));
|
||||||
|
pingWriter.releaseLock();
|
||||||
|
|
||||||
|
// Read PONG response
|
||||||
|
const reader = stream.readable.getReader();
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Gateway closed before receiving PONG"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = Buffer.from(value).toString();
|
||||||
|
|
||||||
|
if (response !== "PONG\n" && response !== "PONG") {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to Ping. Unexpected response: ${response}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.releaseLock();
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err as Error;
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, DEFAULT_RETRY_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await quicClient.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(lastError);
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TProxyServer {
|
||||||
|
server: net.Server;
|
||||||
|
port: number;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupProxyServer = async ({
|
||||||
|
targetPort,
|
||||||
|
targetHost,
|
||||||
|
tlsOptions,
|
||||||
|
relayHost,
|
||||||
|
relayPort,
|
||||||
|
identityId,
|
||||||
|
orgId
|
||||||
|
}: {
|
||||||
|
targetHost: string;
|
||||||
|
targetPort: number;
|
||||||
|
relayPort: number;
|
||||||
|
relayHost: string;
|
||||||
|
tlsOptions: TTlsOption;
|
||||||
|
identityId: string;
|
||||||
|
orgId: string;
|
||||||
|
}): Promise<TProxyServer> => {
|
||||||
|
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||||
|
throw new BadRequestError({
|
||||||
|
error: err as Error
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
server.on("connection", async (clientConn) => {
|
||||||
|
try {
|
||||||
|
clientConn.setKeepAlive(true, 30000); // 30 seconds
|
||||||
|
clientConn.setNoDelay(true);
|
||||||
|
|
||||||
|
const stream = quicClient.connection.newStream("bidi");
|
||||||
|
// Send FORWARD-TCP command
|
||||||
|
const forwardWriter = stream.writable.getWriter();
|
||||||
|
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
|
||||||
|
forwardWriter.releaseLock();
|
||||||
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||||
|
// Set up bidirectional copy
|
||||||
|
const setupCopy = async () => {
|
||||||
|
// Client to QUIC
|
||||||
|
// eslint-disable-next-line
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const writer = stream.writable.getWriter();
|
||||||
|
|
||||||
|
// Create a handler for client data
|
||||||
|
clientConn.on("data", async (chunk) => {
|
||||||
|
await writer.write(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle client connection close
|
||||||
|
clientConn.on("end", async () => {
|
||||||
|
await writer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
clientConn.on("error", async (err) => {
|
||||||
|
await writer.abort(err);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
clientConn.destroy();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// QUIC to Client
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const reader = stream.readable.getReader();
|
||||||
|
|
||||||
|
let reading = true;
|
||||||
|
while (reading) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
reading = false;
|
||||||
|
clientConn.end(); // Close client connection when QUIC stream ends
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write data to TCP client
|
||||||
|
const canContinue = clientConn.write(Buffer.from(value));
|
||||||
|
|
||||||
|
// Handle backpressure
|
||||||
|
if (!canContinue) {
|
||||||
|
await new Promise((res) => {
|
||||||
|
clientConn.once("drain", res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
clientConn.destroy();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
await setupCopy();
|
||||||
|
//
|
||||||
|
// Handle connection closure
|
||||||
|
clientConn.on("close", async () => {
|
||||||
|
await stream.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
clientConn?.destroy();
|
||||||
|
await stream.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
clientConn.on("error", (err) => {
|
||||||
|
logger.error(err, "Client socket error");
|
||||||
|
void cleanup();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
clientConn.on("end", cleanup);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "Failed to establish target connection:");
|
||||||
|
clientConn.end();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("error", (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("close", async () => {
|
||||||
|
await quicClient?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
server.listen(0, () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
server.close();
|
||||||
|
reject(new Error("Failed to get server port"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Gateway proxy started");
|
||||||
|
resolve({
|
||||||
|
server,
|
||||||
|
port: address.port,
|
||||||
|
cleanup: async () => {
|
||||||
|
server.close();
|
||||||
|
await quicClient?.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProxyOptions {
|
||||||
|
targetHost: string;
|
||||||
|
targetPort: number;
|
||||||
|
relayHost: string;
|
||||||
|
relayPort: number;
|
||||||
|
tlsOptions: TTlsOption;
|
||||||
|
identityId: string;
|
||||||
|
orgId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withGatewayProxy = async (
|
||||||
|
callback: (port: number) => Promise<void>,
|
||||||
|
options: ProxyOptions
|
||||||
|
): Promise<void> => {
|
||||||
|
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
|
||||||
|
|
||||||
|
// Setup the proxy server
|
||||||
|
const { port, cleanup } = await setupProxyServer({
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
relayPort,
|
||||||
|
relayHost,
|
||||||
|
tlsOptions,
|
||||||
|
identityId,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute the callback with the allocated port
|
||||||
|
await callback(port);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "Failed to proxy");
|
||||||
|
throw new BadRequestError({ message: (err as Error)?.message });
|
||||||
|
} finally {
|
||||||
|
// Ensure cleanup happens regardless of success or failure
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
};
|
@@ -103,6 +103,16 @@ export const isValidIpOrCidr = (ip: string): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isValidIp = (ip: string) => {
|
||||||
|
return net.isIPv4(ip) || net.isIPv6(ip);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidHostname = (name: string) => {
|
||||||
|
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||||
|
|
||||||
|
return hostnameRegex.test(name);
|
||||||
|
};
|
||||||
|
|
||||||
export type TIp = {
|
export type TIp = {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
type: IPType;
|
type: IPType;
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import opentelemetry, { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
import opentelemetry, { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
||||||
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
||||||
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
||||||
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
||||||
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
||||||
|
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
||||||
import { Resource } from "@opentelemetry/resources";
|
import { Resource } from "@opentelemetry/resources";
|
||||||
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
||||||
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
||||||
|
import tracer from "dd-trace";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
import { initEnvConfig } from "../config/env";
|
import { initEnvConfig } from "../config/env";
|
||||||
@@ -69,7 +70,7 @@ const initTelemetryInstrumentation = ({
|
|||||||
opentelemetry.metrics.setGlobalMeterProvider(meterProvider);
|
opentelemetry.metrics.setGlobalMeterProvider(meterProvider);
|
||||||
|
|
||||||
registerInstrumentations({
|
registerInstrumentations({
|
||||||
instrumentations: [getNodeAutoInstrumentations()]
|
instrumentations: [new HttpInstrumentation()]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,6 +87,17 @@ const setupTelemetry = () => {
|
|||||||
exportType: appCfg.OTEL_EXPORT_TYPE
|
exportType: appCfg.OTEL_EXPORT_TYPE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appCfg.SHOULD_USE_DATADOG_TRACER) {
|
||||||
|
console.log("Initializing Datadog tracer");
|
||||||
|
tracer.init({
|
||||||
|
profiling: appCfg.DATADOG_PROFILING_ENABLED,
|
||||||
|
version: appCfg.INFISICAL_PLATFORM_VERSION,
|
||||||
|
env: appCfg.DATADOG_ENV,
|
||||||
|
service: appCfg.DATADOG_SERVICE,
|
||||||
|
hostname: appCfg.DATADOG_HOSTNAME
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void setupTelemetry();
|
void setupTelemetry();
|
||||||
|
16
backend/src/lib/turn/credentials.ts
Normal file
16
backend/src/lib/turn/credentials.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds
|
||||||
|
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
|
||||||
|
const timestamp = Math.floor((Date.now() + ttl) / 1000);
|
||||||
|
const username = `${timestamp}:${id}`;
|
||||||
|
|
||||||
|
const hmac = crypto.createHmac("sha1", authSecret);
|
||||||
|
hmac.update(username);
|
||||||
|
const password = hmac.digest("base64");
|
||||||
|
|
||||||
|
return {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
};
|
||||||
|
};
|
@@ -83,6 +83,14 @@ const run = async () => {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
logger.error(error, "CRITICAL ERROR: Uncaught Exception");
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (error) => {
|
||||||
|
logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection");
|
||||||
|
});
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: envConfig.PORT,
|
port: envConfig.PORT,
|
||||||
host: envConfig.HOST,
|
host: envConfig.HOST,
|
||||||
|
@@ -27,6 +27,10 @@ import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-
|
|||||||
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
|
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
|
||||||
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||||
|
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
|
||||||
|
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
|
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
|
||||||
|
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
|
||||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
@@ -35,6 +39,12 @@ import { HsmModule } from "@app/ee/services/hsm/hsm-types";
|
|||||||
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
|
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
|
||||||
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||||
|
import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal";
|
||||||
|
import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
|
||||||
|
import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
|
||||||
|
import { kmipOrgConfigDALFactory } from "@app/ee/services/kmip/kmip-org-config-dal";
|
||||||
|
import { kmipOrgServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-org-server-certificate-dal";
|
||||||
|
import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
|
||||||
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
||||||
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||||
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
|
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
|
||||||
@@ -382,6 +392,14 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const projectTemplateDAL = projectTemplateDALFactory(db);
|
const projectTemplateDAL = projectTemplateDALFactory(db);
|
||||||
const resourceMetadataDAL = resourceMetadataDALFactory(db);
|
const resourceMetadataDAL = resourceMetadataDALFactory(db);
|
||||||
|
const kmipClientDAL = kmipClientDALFactory(db);
|
||||||
|
const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db);
|
||||||
|
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
|
||||||
|
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
|
||||||
|
|
||||||
|
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
|
||||||
|
const gatewayDAL = gatewayDALFactory(db);
|
||||||
|
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
@@ -1078,7 +1096,9 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
smtpService,
|
||||||
|
userDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||||
@@ -1290,7 +1310,19 @@ export const registerRoutes = async (
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
const gatewayService = gatewayServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
gatewayDAL,
|
||||||
|
kmsService,
|
||||||
|
licenseService,
|
||||||
|
orgGatewayConfigDAL,
|
||||||
|
keyStore,
|
||||||
|
projectGatewayDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicSecretProviders = buildDynamicSecretProviders({
|
||||||
|
gatewayService
|
||||||
|
});
|
||||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||||
queueService,
|
queueService,
|
||||||
dynamicSecretLeaseDAL,
|
dynamicSecretLeaseDAL,
|
||||||
@@ -1308,8 +1340,10 @@ export const registerRoutes = async (
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService
|
kmsService,
|
||||||
|
projectGatewayDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -1429,6 +1463,24 @@ export const registerRoutes = async (
|
|||||||
keyStore
|
keyStore
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const kmipService = kmipServiceFactory({
|
||||||
|
kmipClientDAL,
|
||||||
|
permissionService,
|
||||||
|
kmipClientCertificateDAL,
|
||||||
|
kmipOrgConfigDAL,
|
||||||
|
kmsService,
|
||||||
|
kmipOrgServerCertificateDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmipOperationService = kmipOperationServiceFactory({
|
||||||
|
kmsService,
|
||||||
|
kmsDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmipClientDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@@ -1527,7 +1579,10 @@ export const registerRoutes = async (
|
|||||||
projectTemplate: projectTemplateService,
|
projectTemplate: projectTemplateService,
|
||||||
totp: totpService,
|
totp: totpService,
|
||||||
appConnection: appConnectionService,
|
appConnection: appConnectionService,
|
||||||
secretSync: secretSyncService
|
secretSync: secretSyncService,
|
||||||
|
kmip: kmipService,
|
||||||
|
kmipOperation: kmipOperationService,
|
||||||
|
gateway: gatewayService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
@@ -1539,7 +1594,8 @@ export const registerRoutes = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
user: userDAL
|
user: userDAL,
|
||||||
|
kmipClient: kmipClientDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
||||||
|
@@ -111,7 +111,16 @@ export const secretRawSchema = z.object({
|
|||||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
actor: z
|
||||||
|
.object({
|
||||||
|
actorId: z.string().nullable().optional(),
|
||||||
|
actorType: z.string().nullable().optional(),
|
||||||
|
name: z.string().nullable().optional(),
|
||||||
|
membershipId: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ProjectPermissionSchema = z.object({
|
export const ProjectPermissionSchema = z.object({
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||||
@@ -72,7 +73,21 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
message: "At least one login method should be enabled."
|
message: "At least one login method should be enabled."
|
||||||
}),
|
}),
|
||||||
slackClientId: z.string().optional(),
|
slackClientId: z.string().optional(),
|
||||||
slackClientSecret: z.string().optional()
|
slackClientSecret: z.string().optional(),
|
||||||
|
authConsentContent: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.refine((content) => DOMPurify.sanitize(content) === content, {
|
||||||
|
message: "Auth consent content contains unsafe HTML."
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
pageFrameContent: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.refine((content) => DOMPurify.sanitize(content) === content, {
|
||||||
|
message: "Page frame content contains unsafe HTML."
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -196,6 +211,27 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/user-management/users/:userId/admin-access",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.superAdmin.grantServerAdminAccessToUser(req.params.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/encryption-strategies",
|
url: "/encryption-strategies",
|
||||||
|
@@ -1,13 +1,19 @@
|
|||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
CreateAwsConnectionSchema,
|
CreateAwsConnectionSchema,
|
||||||
SanitizedAwsConnectionSchema,
|
SanitizedAwsConnectionSchema,
|
||||||
UpdateAwsConnectionSchema
|
UpdateAwsConnectionSchema
|
||||||
} from "@app/services/app-connection/aws";
|
} from "@app/services/app-connection/aws";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
registerAppConnectionEndpoints({
|
registerAppConnectionEndpoints({
|
||||||
app: AppConnection.AWS,
|
app: AppConnection.AWS,
|
||||||
server,
|
server,
|
||||||
@@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
createSchema: CreateAwsConnectionSchema,
|
createSchema: CreateAwsConnectionSchema,
|
||||||
updateSchema: UpdateAwsConnectionSchema
|
updateSchema: UpdateAwsConnectionSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The below endpoints are not exposed and for Infisical App use
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/kms-keys`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
region: z.nativeEnum(AWSRegion),
|
||||||
|
destination: z.enum([SecretSync.AWSParameterStore, SecretSync.AWSSecretsManager])
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
kmsKeys: z.object({ alias: z.string(), id: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const kmsKeys = await server.services.appConnection.aws.listKmsKeys(
|
||||||
|
{
|
||||||
|
connectionId,
|
||||||
|
...req.query
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
return { kmsKeys };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@@ -2,10 +2,9 @@ import jwt from "jsonwebtoken";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
|
||||||
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -21,18 +20,19 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
|
const { decodedToken } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (req.auth.authMode === AuthMode.JWT) {
|
|
||||||
await server.services.login.logout(req.permission.id, req.auth.tokenVersionId);
|
await server.services.login.logout(decodedToken.userId, decodedToken.tokenVersionId);
|
||||||
}
|
|
||||||
void res.cookie("jid", "", {
|
void res.cookie("jid", "", {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: "/",
|
path: "/",
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
secure: appCfg.HTTPS_ENABLED
|
secure: appCfg.HTTPS_ENABLED
|
||||||
});
|
});
|
||||||
|
|
||||||
return { message: "Successfully logged out" };
|
return { message: "Successfully logged out" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -69,37 +69,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const refreshToken = req.cookies.jid;
|
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (!refreshToken)
|
|
||||||
throw new NotFoundError({
|
|
||||||
name: "AuthTokenNotFound",
|
|
||||||
message: "Failed to find refresh token"
|
|
||||||
});
|
|
||||||
|
|
||||||
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
|
|
||||||
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
|
|
||||||
throw new UnauthorizedError({
|
|
||||||
message: "The token provided is not a refresh token",
|
|
||||||
name: "InvalidToken"
|
|
||||||
});
|
|
||||||
|
|
||||||
const tokenVersion = await server.services.authToken.getUserTokenSessionById(
|
|
||||||
decodedToken.tokenVersionId,
|
|
||||||
decodedToken.userId
|
|
||||||
);
|
|
||||||
if (!tokenVersion)
|
|
||||||
throw new UnauthorizedError({
|
|
||||||
message: "Valid token version not found",
|
|
||||||
name: "InvalidToken"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
|
|
||||||
throw new UnauthorizedError({
|
|
||||||
message: "Token version mismatch",
|
|
||||||
name: "InvalidToken"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{
|
{
|
||||||
|
@@ -37,6 +37,7 @@ import { registerProjectMembershipRouter } from "./project-membership-router";
|
|||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerSecretFolderRouter } from "./secret-folder-router";
|
import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||||
import { registerSecretImportRouter } from "./secret-import-router";
|
import { registerSecretImportRouter } from "./secret-import-router";
|
||||||
|
import { registerSecretRequestsRouter } from "./secret-requests-router";
|
||||||
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
||||||
import { registerSecretTagRouter } from "./secret-tag-router";
|
import { registerSecretTagRouter } from "./secret-tag-router";
|
||||||
import { registerSlackRouter } from "./slack-router";
|
import { registerSlackRouter } from "./slack-router";
|
||||||
@@ -110,7 +111,15 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||||
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
|
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
|
||||||
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
||||||
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
|
|
||||||
|
await server.register(
|
||||||
|
async (secretSharingRouter) => {
|
||||||
|
await secretSharingRouter.register(registerSecretSharingRouter, { prefix: "/shared" });
|
||||||
|
await secretSharingRouter.register(registerSecretRequestsRouter, { prefix: "/requests" });
|
||||||
|
},
|
||||||
|
{ prefix: "/secret-sharing" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
||||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
await server.register(registerCmekRouter, { prefix: "/kms" });
|
||||||
|
@@ -47,7 +47,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash)
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.CREATE.directory)
|
.describe(FOLDERS.CREATE.directory),
|
||||||
|
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -65,7 +66,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
...req.body,
|
...req.body,
|
||||||
projectId: req.body.workspaceId,
|
projectId: req.body.workspaceId,
|
||||||
path
|
path,
|
||||||
|
description: req.body.description
|
||||||
});
|
});
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -76,7 +78,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
environment: req.body.environment,
|
environment: req.body.environment,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
folderName: folder.name,
|
folderName: folder.name,
|
||||||
folderPath: path
|
folderPath: path,
|
||||||
|
...(req.body.description ? { description: req.body.description } : {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -125,7 +128,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash)
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.UPDATE.directory)
|
.describe(FOLDERS.UPDATE.directory),
|
||||||
|
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -196,7 +200,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash)
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.UPDATE.path)
|
.describe(FOLDERS.UPDATE.path),
|
||||||
|
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
270
backend/src/server/routes/v1/secret-requests-router.ts
Normal file
270
backend/src/server/routes/v1/secret-requests-router.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSharingSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||||
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
|
export const registerSecretRequestsRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRequest: SecretSharingSchema.omit({
|
||||||
|
encryptedSecret: true,
|
||||||
|
tag: true,
|
||||||
|
iv: true,
|
||||||
|
encryptedValue: true
|
||||||
|
}).extend({
|
||||||
|
isSecretValueSet: z.boolean(),
|
||||||
|
requester: z.object({
|
||||||
|
organizationName: z.string(),
|
||||||
|
firstName: z.string().nullish(),
|
||||||
|
lastName: z.string().nullish(),
|
||||||
|
username: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRequest = await req.server.services.secretSharing.getSecretRequestById({
|
||||||
|
id: req.params.id,
|
||||||
|
actorOrgId: req.permission?.orgId,
|
||||||
|
actor: req.permission?.type,
|
||||||
|
actorId: req.permission?.id,
|
||||||
|
actorAuthMethod: req.permission?.authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRequest };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:id/set-value",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
secretValue: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRequest: SecretSharingSchema.omit({
|
||||||
|
encryptedSecret: true,
|
||||||
|
tag: true,
|
||||||
|
iv: true,
|
||||||
|
encryptedValue: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({
|
||||||
|
id: req.params.id,
|
||||||
|
actorOrgId: req.permission?.orgId,
|
||||||
|
actor: req.permission?.type,
|
||||||
|
actorId: req.permission?.id,
|
||||||
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
|
secretValue: req.body.secretValue
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRequest };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:id/reveal-value",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRequest: SecretSharingSchema.omit({
|
||||||
|
encryptedSecret: true,
|
||||||
|
tag: true,
|
||||||
|
iv: true,
|
||||||
|
encryptedValue: true
|
||||||
|
}).extend({
|
||||||
|
secretValue: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRequest = await req.server.services.secretSharing.revealSecretRequestValue({
|
||||||
|
id: req.params.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRequest };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRequest: SecretSharingSchema.omit({
|
||||||
|
encryptedSecret: true,
|
||||||
|
tag: true,
|
||||||
|
iv: true,
|
||||||
|
encryptedValue: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRequest = await req.server.services.secretSharing.deleteSharedSecretById({
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
sharedSecretId: req.params.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
type: SecretSharingType.Request
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.SecretRequestDeleted,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
secretRequestId: req.params.id,
|
||||||
|
organizationId: req.permission.orgId,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { secretRequest };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().min(0).max(100).default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(25)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: z.array(SecretSharingSchema),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
type: SecretSharingType.Request,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
secrets,
|
||||||
|
totalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().max(50).optional(),
|
||||||
|
expiresAt: z.string(),
|
||||||
|
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const shareRequest = await req.server.services.secretSharing.createSecretRequest({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SECRET_REQUEST,
|
||||||
|
metadata: {
|
||||||
|
accessType: req.body.accessType,
|
||||||
|
name: req.body.name,
|
||||||
|
id: shareRequest.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.SecretRequestCreated,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
secretRequestId: shareRequest.id,
|
||||||
|
organizationId: req.permission.orgId,
|
||||||
|
secretRequestName: req.body.name,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id: shareRequest.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@app/server/config/rateLimiter";
|
} from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||||
|
|
||||||
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -38,6 +39,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
|
type: SecretSharingType.Share,
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,7 +213,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
sharedSecretId
|
sharedSecretId,
|
||||||
|
type: SecretSharingType.Share
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
|
@@ -20,6 +20,7 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
|||||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
|
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
|
||||||
|
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
import { secretRawSchema } from "../sanitizedSchemas";
|
import { secretRawSchema } from "../sanitizedSchemas";
|
||||||
@@ -536,7 +537,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
.describe(RAW_SECRETS.CREATE.secretReminderRepeatDays),
|
.describe(RAW_SECRETS.CREATE.secretReminderRepeatDays),
|
||||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.CREATE.secretReminderNote)
|
secretReminderNote: z
|
||||||
|
.string()
|
||||||
|
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.describe(RAW_SECRETS.CREATE.secretReminderNote)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.union([
|
200: z.union([
|
||||||
@@ -639,7 +645,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
||||||
metadata: z.record(z.string()).optional(),
|
metadata: z.record(z.string()).optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
secretReminderNote: z
|
||||||
|
.string()
|
||||||
|
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||||
secretReminderRepeatDays: z
|
secretReminderRepeatDays: z
|
||||||
.number()
|
.number()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -2030,6 +2041,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(RAW_SECRETS.UPDATE.secretPath),
|
.describe(RAW_SECRETS.UPDATE.secretPath),
|
||||||
|
mode: z
|
||||||
|
.nativeEnum(SecretUpdateMode)
|
||||||
|
.optional()
|
||||||
|
.default(SecretUpdateMode.FailOnNotFound)
|
||||||
|
.describe(RAW_SECRETS.UPDATE.mode),
|
||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName),
|
secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName),
|
||||||
@@ -2037,11 +2053,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
||||||
.describe(RAW_SECRETS.UPDATE.secretValue),
|
.describe(RAW_SECRETS.UPDATE.secretValue),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.optional()
|
||||||
|
.describe(RAW_SECRETS.UPDATE.secretPath),
|
||||||
secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment),
|
secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment),
|
||||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
||||||
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
|
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
|
||||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
||||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
secretReminderNote: z
|
||||||
|
.string()
|
||||||
|
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
secretReminderRepeatDays: z
|
secretReminderRepeatDays: z
|
||||||
.number()
|
.number()
|
||||||
@@ -2073,7 +2100,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
environment,
|
environment,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
projectId: req.body.workspaceId,
|
projectId: req.body.workspaceId,
|
||||||
secrets: inputSecrets
|
secrets: inputSecrets,
|
||||||
|
mode: req.body.mode
|
||||||
});
|
});
|
||||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||||
return { approval: secretOperation.approval };
|
return { approval: secretOperation.approval };
|
||||||
@@ -2092,15 +2120,39 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
environment: req.body.environment,
|
environment: req.body.environment,
|
||||||
secretPath: req.body.secretPath,
|
secretPath: req.body.secretPath,
|
||||||
secrets: secrets.map((secret) => ({
|
secrets: secrets
|
||||||
secretId: secret.id,
|
.filter((el) => el.version > 1)
|
||||||
secretKey: secret.secretKey,
|
.map((secret) => ({
|
||||||
secretVersion: secret.version,
|
secretId: secret.id,
|
||||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
secretPath: secret.secretPath,
|
||||||
}))
|
secretKey: secret.secretKey,
|
||||||
|
secretVersion: secret.version,
|
||||||
|
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const createdSecrets = secrets.filter((el) => el.version === 1);
|
||||||
|
if (createdSecrets.length) {
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: secrets[0].workspace,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SECRETS,
|
||||||
|
metadata: {
|
||||||
|
environment: req.body.environment,
|
||||||
|
secretPath: req.body.secretPath,
|
||||||
|
secrets: createdSecrets.map((secret) => ({
|
||||||
|
secretId: secret.id,
|
||||||
|
secretPath: secret.secretPath,
|
||||||
|
secretKey: secret.secretKey,
|
||||||
|
secretVersion: secret.version,
|
||||||
|
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretUpdated,
|
event: PostHogEventTypes.SecretUpdated,
|
||||||
|
@@ -22,18 +22,19 @@ import {
|
|||||||
TUpdateAppConnectionDTO,
|
TUpdateAppConnectionDTO,
|
||||||
TValidateAppConnectionCredentials
|
TValidateAppConnectionCredentials
|
||||||
} from "@app/services/app-connection/app-connection-types";
|
} from "@app/services/app-connection/app-connection-types";
|
||||||
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
|
||||||
import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks";
|
|
||||||
import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service";
|
|
||||||
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
|
||||||
import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service";
|
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
|
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||||
|
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||||
|
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||||
|
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||||
|
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||||
|
import { githubConnectionService } from "./github/github-connection-service";
|
||||||
|
|
||||||
export type TAppConnectionServiceFactoryDep = {
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
appConnectionDAL: TAppConnectionDALFactory;
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
@@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
listAvailableAppConnectionsForUser,
|
listAvailableAppConnectionsForUser,
|
||||||
github: githubConnectionService(connectAppConnectionById),
|
github: githubConnectionService(connectAppConnectionById),
|
||||||
gcp: gcpConnectionService(connectAppConnectionById),
|
gcp: gcpConnectionService(connectAppConnectionById),
|
||||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
aws: awsConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
TAwsConnection,
|
TAwsConnection,
|
||||||
TAwsConnectionConfig,
|
TAwsConnectionConfig,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
TGitHubConnectionInput,
|
TGitHubConnectionInput,
|
||||||
TValidateGitHubConnectionCredentials
|
TValidateGitHubConnectionCredentials
|
||||||
} from "@app/services/app-connection/github";
|
} from "@app/services/app-connection/github";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TAzureAppConfigurationConnection,
|
TAzureAppConfigurationConnection,
|
||||||
@@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials =
|
|||||||
| TValidateAzureKeyVaultConnectionCredentials
|
| TValidateAzureKeyVaultConnectionCredentials
|
||||||
| TValidateAzureAppConfigurationConnectionCredentials
|
| TValidateAzureAppConfigurationConnectionCredentials
|
||||||
| TValidateDatabricksConnectionCredentials;
|
| TValidateDatabricksConnectionCredentials;
|
||||||
|
|
||||||
|
export type TListAwsConnectionKmsKeys = {
|
||||||
|
connectionId: string;
|
||||||
|
region: AWSRegion;
|
||||||
|
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||||
|
};
|
||||||
|
@@ -0,0 +1,88 @@
|
|||||||
|
import AWS from "aws-sdk";
|
||||||
|
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||||
|
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TAwsConnection>;
|
||||||
|
|
||||||
|
const listAwsKmsKeys = async (
|
||||||
|
appConnection: TAwsConnection,
|
||||||
|
{ region, destination }: Pick<TListAwsConnectionKmsKeys, "region" | "destination">
|
||||||
|
) => {
|
||||||
|
const { credentials } = await getAwsConnectionConfig(appConnection, region);
|
||||||
|
|
||||||
|
const awsKms = new AWS.KMS({
|
||||||
|
credentials,
|
||||||
|
region
|
||||||
|
});
|
||||||
|
|
||||||
|
const aliasEntries: AWS.KMS.AliasList = [];
|
||||||
|
let aliasMarker: string | undefined;
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await awsKms.listAliases({ Limit: 100, Marker: aliasMarker }).promise();
|
||||||
|
aliasEntries.push(...(response.Aliases || []));
|
||||||
|
aliasMarker = response.NextMarker;
|
||||||
|
} while (aliasMarker);
|
||||||
|
|
||||||
|
const keyMetadataRecord: Record<string, AWS.KMS.KeyMetadata | undefined> = {};
|
||||||
|
for await (const aliasEntry of aliasEntries) {
|
||||||
|
if (aliasEntry.TargetKeyId) {
|
||||||
|
const keyDescription = await awsKms.describeKey({ KeyId: aliasEntry.TargetKeyId }).promise();
|
||||||
|
|
||||||
|
keyMetadataRecord[aliasEntry.TargetKeyId] = keyDescription.KeyMetadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validAliasEntries = aliasEntries.filter((aliasEntry) => {
|
||||||
|
if (!aliasEntry.TargetKeyId) return false;
|
||||||
|
|
||||||
|
if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true;
|
||||||
|
|
||||||
|
if (destination === SecretSync.AWSSecretsManager && aliasEntry.AliasName === "alias/aws/secretsmanager")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (aliasEntry.AliasName?.includes("alias/aws/")) return false;
|
||||||
|
|
||||||
|
const keyMetadata = keyMetadataRecord[aliasEntry.TargetKeyId];
|
||||||
|
|
||||||
|
if (!keyMetadata || keyMetadata.KeyUsage !== "ENCRYPT_DECRYPT" || keyMetadata.KeySpec !== "SYMMETRIC_DEFAULT")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsKeys = validAliasEntries.map((aliasEntry) => {
|
||||||
|
return {
|
||||||
|
id: aliasEntry.TargetKeyId!,
|
||||||
|
alias: aliasEntry.AliasName!
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return kmsKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
|
const listKmsKeys = async (
|
||||||
|
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
|
||||||
|
|
||||||
|
const kmsKeys = await listAwsKmsKeys(appConnection, { region, destination });
|
||||||
|
|
||||||
|
return kmsKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listKmsKeys
|
||||||
|
};
|
||||||
|
};
|
@@ -1,6 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||||
@@ -8,7 +9,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
|
|
||||||
import { AuthModeJwtTokenPayload } from "../auth/auth-type";
|
import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TTokenDALFactory } from "./auth-token-dal";
|
import { TTokenDALFactory } from "./auth-token-dal";
|
||||||
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
|
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
|
||||||
@@ -150,6 +151,40 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
|
|
||||||
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
||||||
|
|
||||||
|
const validateRefreshToken = async (refreshToken?: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!refreshToken)
|
||||||
|
throw new NotFoundError({
|
||||||
|
name: "AuthTokenNotFound",
|
||||||
|
message: "Failed to find refresh token"
|
||||||
|
});
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
|
||||||
|
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "The token provided is not a refresh token",
|
||||||
|
name: "InvalidToken"
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId);
|
||||||
|
|
||||||
|
if (!tokenVersion)
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Valid token version not found",
|
||||||
|
name: "InvalidToken"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Token version mismatch",
|
||||||
|
name: "InvalidToken"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { decodedToken, tokenVersion };
|
||||||
|
};
|
||||||
|
|
||||||
// to parse jwt identity in inject identity plugin
|
// to parse jwt identity in inject identity plugin
|
||||||
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
|
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
|
||||||
const session = await tokenDAL.findOneTokenSession({
|
const session = await tokenDAL.findOneTokenSession({
|
||||||
@@ -188,6 +223,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
clearTokenSessionById,
|
clearTokenSessionById,
|
||||||
getTokenSessionByUser,
|
getTokenSessionByUser,
|
||||||
revokeAllMySessions,
|
revokeAllMySessions,
|
||||||
|
validateRefreshToken,
|
||||||
fnValidateJwtIdentity,
|
fnValidateJwtIdentity,
|
||||||
getUserTokenSessionById
|
getUserTokenSessionById
|
||||||
};
|
};
|
||||||
|
@@ -35,6 +35,7 @@ export enum AuthMode {
|
|||||||
|
|
||||||
export enum ActorType { // would extend to AWS, Azure, ...
|
export enum ActorType { // would extend to AWS, Azure, ...
|
||||||
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
|
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
|
||||||
|
KMIP_CLIENT = "kmipClient",
|
||||||
USER = "user", // userIdentity
|
USER = "user", // userIdentity
|
||||||
SERVICE = "service",
|
SERVICE = "service",
|
||||||
IDENTITY = "identity",
|
IDENTITY = "identity",
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { isValidIp } from "@app/lib/ip";
|
||||||
|
|
||||||
const isValidDate = (dateString: string) => {
|
const isValidDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return !Number.isNaN(date.getTime());
|
return !Number.isNaN(date.getTime());
|
||||||
@@ -25,7 +27,7 @@ export const validateAltNamesField = z
|
|||||||
if (data === "") return true;
|
if (data === "") return true;
|
||||||
// Split and validate each alt name
|
// Split and validate each alt name
|
||||||
return data.split(", ").every((name) => {
|
return data.split(", ").every((name) => {
|
||||||
return hostnameRegex.test(name) || z.string().email().safeParse(name).success;
|
return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -40,3 +40,9 @@ export const isCertChainValid = async (certificates: x509.X509Certificate[]) =>
|
|||||||
// chain.build() implicitly verifies the chain
|
// chain.build() implicitly verifies the chain
|
||||||
return chainItems.length === certificates.length;
|
return chainItems.length === certificates.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) =>
|
||||||
|
certificates
|
||||||
|
.map((cert) => cert.toString("pem"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
@@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -134,7 +134,15 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* Return list of names of apps for Vercel integration
|
* Return list of names of apps for Vercel integration
|
||||||
* This is re-used for getting custom environments for Vercel
|
* This is re-used for getting custom environments for Vercel
|
||||||
*/
|
*/
|
||||||
export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
|
export const getAppsVercel = async ({
|
||||||
|
accessToken,
|
||||||
|
teamId,
|
||||||
|
includeCustomEnvironments
|
||||||
|
}: {
|
||||||
|
teamId?: string | null;
|
||||||
|
accessToken: string;
|
||||||
|
includeCustomEnvironments?: boolean;
|
||||||
|
}) => {
|
||||||
const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = [];
|
const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = [];
|
||||||
|
|
||||||
const limit = "20";
|
const limit = "20";
|
||||||
@@ -145,12 +153,6 @@ export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string |
|
|||||||
projects: {
|
projects: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
customEnvironments?: {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
description: string;
|
|
||||||
slug: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
count: number;
|
count: number;
|
||||||
@@ -159,6 +161,20 @@ export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string |
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getProjectCustomEnvironments = async (projectId: string) => {
|
||||||
|
const { data } = await request.get<{ environments: { id: string; slug: string }[] }>(
|
||||||
|
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${projectId}/custom-environments`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.environments;
|
||||||
|
};
|
||||||
|
|
||||||
while (hasMorePages) {
|
while (hasMorePages) {
|
||||||
const params: { [key: string]: string } = {
|
const params: { [key: string]: string } = {
|
||||||
limit
|
limit
|
||||||
@@ -180,17 +196,38 @@ export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string |
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
data.projects.forEach((a) => {
|
if (includeCustomEnvironments) {
|
||||||
apps.push({
|
const projectsWithCustomEnvironments = await Promise.all(
|
||||||
name: a.name,
|
data.projects.map(async (a) => {
|
||||||
appId: a.id,
|
const customEnvironments = await getProjectCustomEnvironments(a.id);
|
||||||
customEnvironments:
|
|
||||||
a.customEnvironments?.map((env) => ({
|
return {
|
||||||
slug: env.slug,
|
...a,
|
||||||
id: env.id
|
customEnvironments
|
||||||
})) ?? []
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
projectsWithCustomEnvironments.forEach((a) => {
|
||||||
|
apps.push({
|
||||||
|
name: a.name,
|
||||||
|
appId: a.id,
|
||||||
|
customEnvironments:
|
||||||
|
a.customEnvironments?.map((env) => ({
|
||||||
|
slug: env.slug,
|
||||||
|
id: env.id
|
||||||
|
})) ?? []
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
} else {
|
||||||
|
data.projects.forEach((a) => {
|
||||||
|
apps.push({
|
||||||
|
name: a.name,
|
||||||
|
appId: a.id,
|
||||||
|
customEnvironments: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
next = data.pagination.next;
|
next = data.pagination.next;
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user