mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
296 Commits
fix/false-
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
ba4b8801eb | ||
|
27abfa4fff | ||
|
4bc9bca287 | ||
|
612c29225d | ||
|
4d43accc8a | ||
|
e741b63e63 | ||
|
9cde1995c7 | ||
|
3ed3856c85 | ||
|
8054d93851 | ||
|
02dc23425c | ||
|
01534c3525 | ||
|
325ce73b9f | ||
|
1639bda3f6 | ||
|
b5b91c929f | ||
|
9bc549ca8c | ||
|
cedc88d83a | ||
|
f866a810c1 | ||
|
0c034d69ac | ||
|
e29f7f656c | ||
|
858e569d4d | ||
|
5d8f32b774 | ||
|
bb71f5eb7e | ||
|
30b431a255 | ||
|
fd32118685 | ||
|
8528f3fd2a | ||
|
eb358bcafd | ||
|
ffbd29c575 | ||
|
a74f0170da | ||
|
a0fad34a6d | ||
|
f0dc5ec876 | ||
|
c2453f0c84 | ||
|
2819c8519e | ||
|
616b013b12 | ||
|
0b9d890a51 | ||
|
5ba507bc1c | ||
|
0ecc196e5d | ||
|
ddac9f7cc4 | ||
|
f5adc4d9f3 | ||
|
34354994d8 | ||
|
d7c3192099 | ||
|
1576358805 | ||
|
e6103d2d3f | ||
|
8bf8bc77c9 | ||
|
3219723149 | ||
|
74b95d92ab | ||
|
6d3793beff | ||
|
0df41f3391 | ||
|
1acac9d479 | ||
|
0cefd6f837 | ||
|
5e9dc0b98d | ||
|
f632847dc6 | ||
|
faa6d1cf40 | ||
|
7fb18870e3 | ||
|
ae841715e5 | ||
|
baac87c16a | ||
|
291d29ec41 | ||
|
b726187ba3 | ||
|
d98ff32b07 | ||
|
1fa510b32f | ||
|
c57f0d8120 | ||
|
00490f2cff | ||
|
ee58f538c0 | ||
|
0fa20f7839 | ||
|
40ef75d3bd | ||
|
26af13453c | ||
|
ad1f71883d | ||
|
aa39451bc2 | ||
|
f5548b3e8c | ||
|
2659ea7170 | ||
|
d2e3f504fd | ||
|
ca4151a34d | ||
|
d4bc104fd1 | ||
|
7e3a3fcdb0 | ||
|
7f67912d2f | ||
|
a7f4020c08 | ||
|
d2d89034ba | ||
|
2fc6c564c0 | ||
|
240b86c1d5 | ||
|
30d66cef89 | ||
|
b7d11444a9 | ||
|
0a6aef0afa | ||
|
0ac7ec460b | ||
|
808a901aee | ||
|
d5412f916f | ||
|
d0648ca596 | ||
|
3291f1f908 | ||
|
965d30bd03 | ||
|
68fe7c589a | ||
|
54377a44d3 | ||
|
8c902d7699 | ||
|
c25c84aeb3 | ||
|
4359eb7313 | ||
|
322536d738 | ||
|
6c5db3a187 | ||
|
a337e6badd | ||
|
524a97e9a6 | ||
|
c56f598115 | ||
|
19d32a1a3b | ||
|
7e5417a0eb | ||
|
afd6de27fe | ||
|
7781a6b7e7 | ||
|
b3b4e41d92 | ||
|
5225f5136a | ||
|
398adfaf76 | ||
|
d77c26fa38 | ||
|
ef7b81734a | ||
|
09b489a348 | ||
|
6b5c50def0 | ||
|
1f2d52176c | ||
|
7002e297c8 | ||
|
71864a131f | ||
|
9964d2ecaa | ||
|
3ebbaefc2a | ||
|
dd5c494bdb | ||
|
bace8af5a1 | ||
|
f56196b820 | ||
|
7042d73410 | ||
|
cb22ee315e | ||
|
701eb7cfc6 | ||
|
bf8df14b01 | ||
|
1ba8b6394b | ||
|
c442c8483a | ||
|
0435305a68 | ||
|
febf11f502 | ||
|
64fd15c32d | ||
|
a2c9494d52 | ||
|
18460e0678 | ||
|
3d03fece74 | ||
|
234e7eb9be | ||
|
04af313bf0 | ||
|
9b038ccc45 | ||
|
9beb384546 | ||
|
12ec9b4b4e | ||
|
96b8e7fda8 | ||
|
93b9108aa3 | ||
|
99017ea1ae | ||
|
f32588112e | ||
|
f9b0b6700a | ||
|
b45d9398f0 | ||
|
1d1140237f | ||
|
937560fd8d | ||
|
5f4b7b9ea7 | ||
|
05139820a5 | ||
|
7f6bc3ecfe | ||
|
d8cc000ad1 | ||
|
8fc03c06d9 | ||
|
50ceedf39f | ||
|
550096e72b | ||
|
1190ca2d77 | ||
|
2fb60201bc | ||
|
e763a6f683 | ||
|
cb1b006118 | ||
|
356e7c5958 | ||
|
634b500244 | ||
|
54b4d4ae55 | ||
|
1a68765f15 | ||
|
2f6dab3f63 | ||
|
ae07d38c19 | ||
|
e9564f5231 | ||
|
05cdca9202 | ||
|
5ab0c66dee | ||
|
f5a0641671 | ||
|
2843818395 | ||
|
2357f3bc1f | ||
|
cde813aafb | ||
|
bbc8091d44 | ||
|
ce5e591457 | ||
|
5ae74f9761 | ||
|
eef331bbd1 | ||
|
d5c2e9236a | ||
|
025b4b8761 | ||
|
13eef7e524 | ||
|
ef688efc8d | ||
|
8c98565715 | ||
|
f97f98b2c3 | ||
|
e9358cd1d8 | ||
|
3fa84c578c | ||
|
c22ed04733 | ||
|
64fac1979f | ||
|
2d60f389c2 | ||
|
7798e5a2ad | ||
|
ed78227725 | ||
|
89848a2f5c | ||
|
1936f7cc4f | ||
|
1adeb5a70d | ||
|
058475fc3f | ||
|
ee4eb7f84b | ||
|
8122433f5c | ||
|
a0411e3ba8 | ||
|
62968c5e43 | ||
|
f3cf1a3f50 | ||
|
b4b417658f | ||
|
fed99a14a8 | ||
|
d4cfee99a6 | ||
|
e70ca57510 | ||
|
06f321e4bf | ||
|
3c3fcd0db8 | ||
|
21eb2bed7e | ||
|
31a21a432d | ||
|
381960b0bd | ||
|
7eb05afe2a | ||
|
0b54948b15 | ||
|
39e598e408 | ||
|
b735618601 | ||
|
3a5e862def | ||
|
d1c4a9c75a | ||
|
5532844ee7 | ||
|
dd5aab973f | ||
|
ced12baf5d | ||
|
7db1e62654 | ||
|
0ab3ae442e | ||
|
ed9472efc8 | ||
|
e094844601 | ||
|
e761b49964 | ||
|
6a8be75b79 | ||
|
4daaf80caa | ||
|
cf7768d8e5 | ||
|
e76d2f58ea | ||
|
a92e61575d | ||
|
761007208d | ||
|
cc3e0d1922 | ||
|
765280eef6 | ||
|
215761ca6b | ||
|
0977ff1e36 | ||
|
c6081900a4 | ||
|
86800c0cdb | ||
|
1fa99e5585 | ||
|
7947e73569 | ||
|
8f5bb44ff4 | ||
|
3f70f08e8c | ||
|
078eaff164 | ||
|
221aa99374 | ||
|
6a681dcf6a | ||
|
b99b98b6a4 | ||
|
d7271b9631 | ||
|
379e526200 | ||
|
1f151a9b05 | ||
|
52ce90846a | ||
|
be36827392 | ||
|
68a3291235 | ||
|
471f47d260 | ||
|
ccb757ec3e | ||
|
35f7420447 | ||
|
c6a0e36318 | ||
|
181ba75f2a | ||
|
c00f6601bd | ||
|
111605a945 | ||
|
2ac110f00e | ||
|
0366506213 | ||
|
fb253d00eb | ||
|
097512c691 | ||
|
36a13d182f | ||
|
8b26670d73 | ||
|
35d3581e23 | ||
|
f5920f416a | ||
|
3b2154bab4 | ||
|
c5816014a6 | ||
|
48174e2500 | ||
|
7cf297344b | ||
|
0edf0dac98 | ||
|
42249726d4 | ||
|
a757ea22a1 | ||
|
74df374998 | ||
|
925a594a1b | ||
|
36af975594 | ||
|
ee54d460a0 | ||
|
3c32d8dd90 | ||
|
9b50d451ec | ||
|
7ede4e2cf5 | ||
|
4552f0efa4 | ||
|
0d35273857 | ||
|
5ad8dab250 | ||
|
92a80b3314 | ||
|
01dcbb0122 | ||
|
adb0819102 | ||
|
41ba111a69 | ||
|
1b48ce21be | ||
|
2f922d6343 | ||
|
e67b0540dd | ||
|
a78455fde6 | ||
|
967dac9be6 | ||
|
922b245780 | ||
|
ec1ce3dc06 | ||
|
82a4b89bb5 | ||
|
ff3d8c896b | ||
|
6e720c2f64 | ||
|
5b618b07fa | ||
|
a5a1f57284 | ||
|
8327f6154e | ||
|
20a9fc113c | ||
|
8edfa9ad0b | ||
|
00ce755996 | ||
|
3b2173a098 | ||
|
07d9398aad | ||
|
4fc8c509ac | ||
|
242595fceb |
17
.env.example
17
.env.example
@@ -88,3 +88,20 @@ PLAIN_WISH_LABEL_IDS=
|
|||||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||||
|
|
||||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
||||||
|
|
||||||
|
# App Connections
|
||||||
|
|
||||||
|
# aws assume-role
|
||||||
|
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID=
|
||||||
|
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY=
|
||||||
|
|
||||||
|
# github oauth
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID=
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET=
|
||||||
|
|
||||||
|
#github app
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_ID=
|
4
.github/workflows/check-fe-ts-and-lint.yml
vendored
4
.github/workflows/check-fe-ts-and-lint.yml
vendored
@@ -18,10 +18,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 🔧 Setup Node 16
|
- name: 🔧 Setup Node 20
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
node-version: "20"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
- name: 📦 Install dependencies
|
- name: 📦 Install dependencies
|
||||||
|
6
.github/workflows/deployment-pipeline.yml
vendored
6
.github/workflows/deployment-pipeline.yml
vendored
@@ -97,7 +97,7 @@ jobs:
|
|||||||
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
environment-variables: "LOG_LEVEL=info"
|
environment-variables: "LOG_LEVEL=info"
|
||||||
- name: Deploy to Amazon ECS service
|
- name: Deploy to Amazon ECS service
|
||||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
||||||
with:
|
with:
|
||||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||||
service: infisical-core-gamma-stage
|
service: infisical-core-gamma-stage
|
||||||
@@ -153,7 +153,7 @@ jobs:
|
|||||||
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
environment-variables: "LOG_LEVEL=info"
|
environment-variables: "LOG_LEVEL=info"
|
||||||
- name: Deploy to Amazon ECS service
|
- name: Deploy to Amazon ECS service
|
||||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
||||||
with:
|
with:
|
||||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||||
service: infisical-core-platform
|
service: infisical-core-platform
|
||||||
@@ -204,7 +204,7 @@ jobs:
|
|||||||
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
environment-variables: "LOG_LEVEL=info"
|
environment-variables: "LOG_LEVEL=info"
|
||||||
- name: Deploy to Amazon ECS service
|
- name: Deploy to Amazon ECS service
|
||||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
||||||
with:
|
with:
|
||||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||||
service: infisical-core-platform
|
service: infisical-core-platform
|
||||||
|
@@ -8,7 +8,7 @@ FROM node:20-slim AS base
|
|||||||
FROM base AS frontend-dependencies
|
FROM base AS frontend-dependencies
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci --only-production --ignore-scripts
|
RUN npm ci --only-production --ignore-scripts
|
||||||
@@ -23,17 +23,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
|||||||
COPY /frontend .
|
COPY /frontend .
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV NEXT_PUBLIC_ENV production
|
|
||||||
ARG POSTHOG_HOST
|
ARG POSTHOG_HOST
|
||||||
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
ENV VITE_POSTHOG_HOST $POSTHOG_HOST
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||||
ARG INTERCOM_ID
|
ARG INTERCOM_ID
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
ENV VITE_INTERCOM_ID $INTERCOM_ID
|
||||||
ARG INFISICAL_PLATFORM_VERSION
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@@ -44,20 +43,10 @@ WORKDIR /app
|
|||||||
|
|
||||||
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|
||||||
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
||||||
VOLUME /app/.next/cache/images
|
|
||||||
|
|
||||||
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
|
||||||
COPY --from=frontend-builder /app/public ./public
|
|
||||||
RUN chown non-root-user:nodejs ./public/data
|
|
||||||
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
|
||||||
|
|
||||||
USER non-root-user
|
USER non-root-user
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## BACKEND
|
## BACKEND
|
||||||
##
|
##
|
||||||
@@ -137,6 +126,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
freetds-dev \
|
freetds-dev \
|
||||||
freetds-bin \
|
freetds-bin \
|
||||||
tdsodbc \
|
tdsodbc \
|
||||||
|
openssh-client \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Configure ODBC in production
|
# Configure ODBC in production
|
||||||
@@ -159,14 +149,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
|
|||||||
|
|
||||||
## set pre baked keys
|
## set pre baked keys
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
|
||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
ENV INTERCOM_ID=$INTERCOM_ID
|
||||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@ RUN apk add --no-cache libc6-compat
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci --only-production --ignore-scripts
|
RUN npm ci --only-production --ignore-scripts
|
||||||
@@ -27,17 +27,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
|||||||
COPY /frontend .
|
COPY /frontend .
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV NEXT_PUBLIC_ENV production
|
|
||||||
ARG POSTHOG_HOST
|
ARG POSTHOG_HOST
|
||||||
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
ENV VITE_POSTHOG_HOST $POSTHOG_HOST
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||||
ARG INTERCOM_ID
|
ARG INTERCOM_ID
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
ENV VITE_INTERCOM_ID $INTERCOM_ID
|
||||||
ARG INFISICAL_PLATFORM_VERSION
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@@ -49,20 +48,10 @@ WORKDIR /app
|
|||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 non-root-user
|
RUN adduser --system --uid 1001 non-root-user
|
||||||
|
|
||||||
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
||||||
VOLUME /app/.next/cache/images
|
|
||||||
|
|
||||||
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
|
||||||
COPY --from=frontend-builder /app/public ./public
|
|
||||||
RUN chown non-root-user:nodejs ./public/data
|
|
||||||
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
|
||||||
|
|
||||||
USER non-root-user
|
USER non-root-user
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## BACKEND
|
## BACKEND
|
||||||
##
|
##
|
||||||
@@ -139,7 +128,8 @@ RUN apk --update add \
|
|||||||
freetds-dev \
|
freetds-dev \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
git
|
git \
|
||||||
|
openssh
|
||||||
|
|
||||||
# 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/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
@@ -158,14 +148,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
|
|||||||
|
|
||||||
## set pre baked keys
|
## set pre baked keys
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
|
||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
ENV INTERCOM_ID=$INTERCOM_ID
|
||||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
|
||||||
|
|
||||||
|
|
||||||
COPY --from=backend-runner /app /backend
|
COPY --from=backend-runner /app /backend
|
||||||
|
@@ -7,7 +7,8 @@ WORKDIR /app
|
|||||||
RUN apk --update add \
|
RUN apk --update add \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++
|
g++ \
|
||||||
|
openssh
|
||||||
|
|
||||||
# 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 apk add --no-cache \
|
||||||
|
@@ -17,7 +17,8 @@ RUN apk --update add \
|
|||||||
openssl-dev \
|
openssl-dev \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++
|
g++ \
|
||||||
|
openssh
|
||||||
|
|
||||||
# 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 apk add --no-cache \
|
||||||
|
@@ -22,8 +22,10 @@ export const mockQueue = (): TQueueServiceFactory => {
|
|||||||
listen: (name, event) => {
|
listen: (name, event) => {
|
||||||
events[name] = event;
|
events[name] = event;
|
||||||
},
|
},
|
||||||
|
getRepeatableJobs: async () => [],
|
||||||
clearQueue: async () => {},
|
clearQueue: async () => {},
|
||||||
stopJobById: async () => {},
|
stopJobById: async () => {},
|
||||||
stopRepeatableJobByJobId: async () => true
|
stopRepeatableJobByJobId: async () => true,
|
||||||
|
stopRepeatableJobByKey: async () => true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
128
backend/package-lock.json
generated
128
backend/package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/request-context": "^5.1.0",
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
|
"@fastify/static": "^7.0.4",
|
||||||
"@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",
|
||||||
@@ -5406,6 +5407,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
|
||||||
"integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==",
|
"integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
@@ -5545,6 +5547,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
|
||||||
"integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==",
|
"integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lukeed/ms": "^2.0.1",
|
"@lukeed/ms": "^2.0.1",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
@@ -5563,16 +5566,85 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fastify/static": {
|
"node_modules/@fastify/static": {
|
||||||
"version": "6.12.0",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz",
|
||||||
"integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==",
|
"integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/accept-negotiator": "^1.0.0",
|
"@fastify/accept-negotiator": "^1.0.0",
|
||||||
"@fastify/send": "^2.0.0",
|
"@fastify/send": "^2.0.0",
|
||||||
"content-disposition": "^0.5.3",
|
"content-disposition": "^0.5.3",
|
||||||
"fastify-plugin": "^4.0.0",
|
"fastify-plugin": "^4.0.0",
|
||||||
"glob": "^8.0.1",
|
"fastq": "^1.17.0",
|
||||||
"p-limit": "^3.1.0"
|
"glob": "^10.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/glob": {
|
||||||
|
"version": "10.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
|
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.1.0",
|
||||||
|
"jackspeak": "^3.1.2",
|
||||||
|
"minimatch": "^9.0.4",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^1.11.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/jackspeak": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/cliui": "^8.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/minimatch": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fastify/swagger": {
|
"node_modules/@fastify/swagger": {
|
||||||
@@ -5599,6 +5671,20 @@
|
|||||||
"yaml": "^2.2.2"
|
"yaml": "^2.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/swagger-ui/node_modules/@fastify/static": {
|
||||||
|
"version": "6.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz",
|
||||||
|
"integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/accept-negotiator": "^1.0.0",
|
||||||
|
"@fastify/send": "^2.0.0",
|
||||||
|
"content-disposition": "^0.5.3",
|
||||||
|
"fastify-plugin": "^4.0.0",
|
||||||
|
"glob": "^8.0.1",
|
||||||
|
"p-limit": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@google-cloud/kms": {
|
"node_modules/@google-cloud/kms": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
|
||||||
@@ -6062,9 +6148,10 @@
|
|||||||
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
|
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@lukeed/ms": {
|
"node_modules/@lukeed/ms": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
|
||||||
"integrity": "sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==",
|
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -13879,9 +13966,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.1",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
@@ -13903,7 +13990,7 @@
|
|||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.10",
|
"path-to-regexp": "0.1.12",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.13.0",
|
"qs": "6.13.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
@@ -13918,6 +14005,10 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-session": {
|
"node_modules/express-session": {
|
||||||
@@ -17388,15 +17479,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.cjs"
|
||||||
},
|
},
|
||||||
@@ -18383,9 +18475,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.10",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
|
@@ -134,6 +134,7 @@
|
|||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/request-context": "^5.1.0",
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
|
"@fastify/static": "^7.0.4",
|
||||||
"@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",
|
||||||
|
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@@ -31,9 +31,12 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
|
|||||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
|
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||||
|
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||||
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
@@ -177,6 +180,8 @@ declare module "fastify" {
|
|||||||
auditLogStream: TAuditLogStreamServiceFactory;
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
certificate: TCertificateServiceFactory;
|
certificate: TCertificateServiceFactory;
|
||||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||||
|
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
||||||
|
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
certificateEst: TCertificateEstServiceFactory;
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
@@ -204,6 +209,7 @@ declare module "fastify" {
|
|||||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||||
projectTemplate: TProjectTemplateServiceFactory;
|
projectTemplate: TProjectTemplateServiceFactory;
|
||||||
totp: TTotpServiceFactory;
|
totp: TTotpServiceFactory;
|
||||||
|
appConnection: TAppConnectionServiceFactory;
|
||||||
};
|
};
|
||||||
// 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
|
||||||
|
54
backend/src/@types/knex.d.ts
vendored
54
backend/src/@types/knex.d.ts
vendored
@@ -218,6 +218,9 @@ import {
|
|||||||
TRateLimit,
|
TRateLimit,
|
||||||
TRateLimitInsert,
|
TRateLimitInsert,
|
||||||
TRateLimitUpdate,
|
TRateLimitUpdate,
|
||||||
|
TResourceMetadata,
|
||||||
|
TResourceMetadataInsert,
|
||||||
|
TResourceMetadataUpdate,
|
||||||
TSamlConfigs,
|
TSamlConfigs,
|
||||||
TSamlConfigsInsert,
|
TSamlConfigsInsert,
|
||||||
TSamlConfigsUpdate,
|
TSamlConfigsUpdate,
|
||||||
@@ -317,6 +320,21 @@ import {
|
|||||||
TSlackIntegrations,
|
TSlackIntegrations,
|
||||||
TSlackIntegrationsInsert,
|
TSlackIntegrationsInsert,
|
||||||
TSlackIntegrationsUpdate,
|
TSlackIntegrationsUpdate,
|
||||||
|
TSshCertificateAuthorities,
|
||||||
|
TSshCertificateAuthoritiesInsert,
|
||||||
|
TSshCertificateAuthoritiesUpdate,
|
||||||
|
TSshCertificateAuthoritySecrets,
|
||||||
|
TSshCertificateAuthoritySecretsInsert,
|
||||||
|
TSshCertificateAuthoritySecretsUpdate,
|
||||||
|
TSshCertificateBodies,
|
||||||
|
TSshCertificateBodiesInsert,
|
||||||
|
TSshCertificateBodiesUpdate,
|
||||||
|
TSshCertificates,
|
||||||
|
TSshCertificatesInsert,
|
||||||
|
TSshCertificatesUpdate,
|
||||||
|
TSshCertificateTemplates,
|
||||||
|
TSshCertificateTemplatesInsert,
|
||||||
|
TSshCertificateTemplatesUpdate,
|
||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
@@ -348,6 +366,7 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections";
|
||||||
import {
|
import {
|
||||||
TExternalGroupOrgRoleMappings,
|
TExternalGroupOrgRoleMappings,
|
||||||
TExternalGroupOrgRoleMappingsInsert,
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
@@ -378,6 +397,31 @@ declare module "knex/types/tables" {
|
|||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateAuthorities,
|
||||||
|
TSshCertificateAuthoritiesInsert,
|
||||||
|
TSshCertificateAuthoritiesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificateAuthoritySecret]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateAuthoritySecrets,
|
||||||
|
TSshCertificateAuthoritySecretsInsert,
|
||||||
|
TSshCertificateAuthoritySecretsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateTemplates,
|
||||||
|
TSshCertificateTemplatesInsert,
|
||||||
|
TSshCertificateTemplatesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificate]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificates,
|
||||||
|
TSshCertificatesInsert,
|
||||||
|
TSshCertificatesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificateBody]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateBodies,
|
||||||
|
TSshCertificateBodiesInsert,
|
||||||
|
TSshCertificateBodiesUpdate
|
||||||
|
>;
|
||||||
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateAuthorities,
|
TCertificateAuthorities,
|
||||||
TCertificateAuthoritiesInsert,
|
TCertificateAuthoritiesInsert,
|
||||||
@@ -846,5 +890,15 @@ declare module "knex/types/tables" {
|
|||||||
TProjectSplitBackfillIdsInsert,
|
TProjectSplitBackfillIdsInsert,
|
||||||
TProjectSplitBackfillIdsUpdate
|
TProjectSplitBackfillIdsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.ResourceMetadata]: KnexOriginal.CompositeTableType<
|
||||||
|
TResourceMetadata,
|
||||||
|
TResourceMetadataInsert,
|
||||||
|
TResourceMetadataUpdate
|
||||||
|
>;
|
||||||
|
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
|
||||||
|
TAppConnections,
|
||||||
|
TAppConnectionsInsert,
|
||||||
|
TAppConnectionsUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
99
backend/src/db/migrations/20241216013357_ssh-mgmt.ts
Normal file
99
backend/src/db/migrations/20241216013357_ssh-mgmt.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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.SshCertificateAuthority))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.string("status").notNullable(); // active / disabled
|
||||||
|
t.string("friendlyName").notNullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateAuthority);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateAuthoritySecret))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateAuthoritySecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCaId").notNullable().unique();
|
||||||
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedPrivateKey").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateTemplate))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateTemplate, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCaId").notNullable();
|
||||||
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.string("status").notNullable(); // active / disabled
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.string("ttl").notNullable();
|
||||||
|
t.string("maxTTL").notNullable();
|
||||||
|
t.specificType("allowedUsers", "text[]").notNullable();
|
||||||
|
t.specificType("allowedHosts", "text[]").notNullable();
|
||||||
|
t.boolean("allowUserCertificates").notNullable();
|
||||||
|
t.boolean("allowHostCertificates").notNullable();
|
||||||
|
t.boolean("allowCustomKeyIds").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificate))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificate, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCaId").notNullable();
|
||||||
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||||
|
t.uuid("sshCertificateTemplateId");
|
||||||
|
t.foreign("sshCertificateTemplateId")
|
||||||
|
.references("id")
|
||||||
|
.inTable(TableName.SshCertificateTemplate)
|
||||||
|
.onDelete("SET NULL");
|
||||||
|
t.string("serialNumber").notNullable().unique();
|
||||||
|
t.string("certType").notNullable(); // user or host
|
||||||
|
t.specificType("principals", "text[]").notNullable();
|
||||||
|
t.string("keyId").notNullable();
|
||||||
|
t.datetime("notBefore").notNullable();
|
||||||
|
t.datetime("notAfter").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateBody))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateBody, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCertId").notNullable().unique();
|
||||||
|
t.foreign("sshCertId").references("id").inTable(TableName.SshCertificate).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedCertificate").notNullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateBody);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateBody);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificate);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificate);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateTemplate);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateAuthoritySecret);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateAuthority);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthority);
|
||||||
|
}
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ResourceMetadata))) {
|
||||||
|
await knex.schema.createTable(TableName.ResourceMetadata, (tb) => {
|
||||||
|
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
tb.string("key").notNullable();
|
||||||
|
tb.string("value", 1020).notNullable();
|
||||||
|
tb.uuid("orgId").notNullable();
|
||||||
|
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
tb.uuid("userId");
|
||||||
|
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
tb.uuid("identityId");
|
||||||
|
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
tb.uuid("secretId");
|
||||||
|
tb.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
|
||||||
|
tb.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
|
||||||
|
if (!hasSecretMetadataField) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
|
||||||
|
t.jsonb("secretMetadata");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ResourceMetadata);
|
||||||
|
|
||||||
|
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
|
||||||
|
if (hasSecretMetadataField) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
|
||||||
|
t.dropColumn("secretMetadata");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
backend/src/db/migrations/20241218181018_app-connection.ts
Normal file
28
backend/src/db/migrations/20241218181018_app-connection.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.AppConnection))) {
|
||||||
|
await knex.schema.createTable(TableName.AppConnection, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name", 32).notNullable();
|
||||||
|
t.string("description");
|
||||||
|
t.string("app").notNullable();
|
||||||
|
t.string("method").notNullable();
|
||||||
|
t.binary("encryptedCredentials").notNullable();
|
||||||
|
t.integer("version").defaultTo(1).notNullable();
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.AppConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.AppConnection);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.AppConnection);
|
||||||
|
}
|
27
backend/src/db/schemas/app-connections.ts
Normal file
27
backend/src/db/schemas/app-connections.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// 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 AppConnectionsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
app: z.string(),
|
||||||
|
method: z.string(),
|
||||||
|
encryptedCredentials: zodBuffer,
|
||||||
|
version: z.number().default(1),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||||
|
export type TAppConnectionsInsert = Omit<z.input<typeof AppConnectionsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TAppConnectionsUpdate = Partial<Omit<z.input<typeof AppConnectionsSchema>, TImmutableDBKeys>>;
|
@@ -71,6 +71,7 @@ export * from "./project-user-additional-privilege";
|
|||||||
export * from "./project-user-membership-roles";
|
export * from "./project-user-membership-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./rate-limit";
|
export * from "./rate-limit";
|
||||||
|
export * from "./resource-metadata";
|
||||||
export * from "./saml-configs";
|
export * from "./saml-configs";
|
||||||
export * from "./scim-tokens";
|
export * from "./scim-tokens";
|
||||||
export * from "./secret-approval-policies";
|
export * from "./secret-approval-policies";
|
||||||
@@ -107,6 +108,11 @@ export * from "./secrets";
|
|||||||
export * from "./secrets-v2";
|
export * from "./secrets-v2";
|
||||||
export * from "./service-tokens";
|
export * from "./service-tokens";
|
||||||
export * from "./slack-integrations";
|
export * from "./slack-integrations";
|
||||||
|
export * from "./ssh-certificate-authorities";
|
||||||
|
export * from "./ssh-certificate-authority-secrets";
|
||||||
|
export * from "./ssh-certificate-bodies";
|
||||||
|
export * from "./ssh-certificate-templates";
|
||||||
|
export * from "./ssh-certificates";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
export * from "./totp-configs";
|
export * from "./totp-configs";
|
||||||
export * from "./trusted-ips";
|
export * from "./trusted-ips";
|
||||||
|
@@ -2,6 +2,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
SshCertificateAuthority = "ssh_certificate_authorities",
|
||||||
|
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
|
||||||
|
SshCertificateTemplate = "ssh_certificate_templates",
|
||||||
|
SshCertificate = "ssh_certificates",
|
||||||
|
SshCertificateBody = "ssh_certificate_bodies",
|
||||||
CertificateAuthority = "certificate_authorities",
|
CertificateAuthority = "certificate_authorities",
|
||||||
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||||
CertificateAuthorityCert = "certificate_authority_certs",
|
CertificateAuthorityCert = "certificate_authority_certs",
|
||||||
@@ -75,6 +80,7 @@ export enum TableName {
|
|||||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||||
// used by both identity and users
|
// used by both identity and users
|
||||||
IdentityMetadata = "identity_metadata",
|
IdentityMetadata = "identity_metadata",
|
||||||
|
ResourceMetadata = "resource_metadata",
|
||||||
ScimToken = "scim_tokens",
|
ScimToken = "scim_tokens",
|
||||||
AccessApprovalPolicy = "access_approval_policies",
|
AccessApprovalPolicy = "access_approval_policies",
|
||||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||||
@@ -124,7 +130,8 @@ export enum TableName {
|
|||||||
KmsKeyVersion = "kms_key_versions",
|
KmsKeyVersion = "kms_key_versions",
|
||||||
WorkflowIntegrations = "workflow_integrations",
|
WorkflowIntegrations = "workflow_integrations",
|
||||||
SlackIntegrations = "slack_integrations",
|
SlackIntegrations = "slack_integrations",
|
||||||
ProjectSlackConfigs = "project_slack_configs"
|
ProjectSlackConfigs = "project_slack_configs",
|
||||||
|
AppConnection = "app_connections"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
@@ -205,5 +212,6 @@ export enum IdentityAuthMethod {
|
|||||||
export enum ProjectType {
|
export enum ProjectType {
|
||||||
SecretManager = "secret-manager",
|
SecretManager = "secret-manager",
|
||||||
CertificateManager = "cert-manager",
|
CertificateManager = "cert-manager",
|
||||||
KMS = "kms"
|
KMS = "kms",
|
||||||
|
SSH = "ssh"
|
||||||
}
|
}
|
||||||
|
24
backend/src/db/schemas/resource-metadata.ts
Normal file
24
backend/src/db/schemas/resource-metadata.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const ResourceMetadataSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid().nullable().optional(),
|
||||||
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
|
secretId: z.string().uuid().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
|
||||||
|
export type TResourceMetadataInsert = Omit<z.input<typeof ResourceMetadataSchema>, TImmutableDBKeys>;
|
||||||
|
export type TResourceMetadataUpdate = Partial<Omit<z.input<typeof ResourceMetadataSchema>, TImmutableDBKeys>>;
|
@@ -24,7 +24,8 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({
|
|||||||
requestId: z.string().uuid(),
|
requestId: z.string().uuid(),
|
||||||
op: z.string(),
|
op: z.string(),
|
||||||
secretId: z.string().uuid().nullable().optional(),
|
secretId: z.string().uuid().nullable().optional(),
|
||||||
secretVersion: z.string().uuid().nullable().optional()
|
secretVersion: z.string().uuid().nullable().optional(),
|
||||||
|
secretMetadata: z.unknown().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;
|
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;
|
||||||
|
24
backend/src/db/schemas/ssh-certificate-authorities.ts
Normal file
24
backend/src/db/schemas/ssh-certificate-authorities.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshCertificateAuthoritiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
projectId: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
friendlyName: z.string(),
|
||||||
|
keyAlgorithm: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
||||||
|
export type TSshCertificateAuthoritiesInsert = Omit<z.input<typeof SshCertificateAuthoritiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificateAuthoritiesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
27
backend/src/db/schemas/ssh-certificate-authority-secrets.ts
Normal file
27
backend/src/db/schemas/ssh-certificate-authority-secrets.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// 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 SshCertificateAuthoritySecretsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCaId: z.string().uuid(),
|
||||||
|
encryptedPrivateKey: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateAuthoritySecrets = z.infer<typeof SshCertificateAuthoritySecretsSchema>;
|
||||||
|
export type TSshCertificateAuthoritySecretsInsert = Omit<
|
||||||
|
z.input<typeof SshCertificateAuthoritySecretsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TSshCertificateAuthoritySecretsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateAuthoritySecretsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
22
backend/src/db/schemas/ssh-certificate-bodies.ts
Normal file
22
backend/src/db/schemas/ssh-certificate-bodies.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshCertificateBodiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCertId: z.string().uuid(),
|
||||||
|
encryptedCertificate: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateBodies = z.infer<typeof SshCertificateBodiesSchema>;
|
||||||
|
export type TSshCertificateBodiesInsert = Omit<z.input<typeof SshCertificateBodiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificateBodiesUpdate = Partial<Omit<z.input<typeof SshCertificateBodiesSchema>, TImmutableDBKeys>>;
|
30
backend/src/db/schemas/ssh-certificate-templates.ts
Normal file
30
backend/src/db/schemas/ssh-certificate-templates.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// 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 SshCertificateTemplatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCaId: z.string().uuid(),
|
||||||
|
status: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
ttl: z.string(),
|
||||||
|
maxTTL: z.string(),
|
||||||
|
allowedUsers: z.string().array(),
|
||||||
|
allowedHosts: z.string().array(),
|
||||||
|
allowUserCertificates: z.boolean(),
|
||||||
|
allowHostCertificates: z.boolean(),
|
||||||
|
allowCustomKeyIds: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateTemplates = z.infer<typeof SshCertificateTemplatesSchema>;
|
||||||
|
export type TSshCertificateTemplatesInsert = Omit<z.input<typeof SshCertificateTemplatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificateTemplatesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateTemplatesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
26
backend/src/db/schemas/ssh-certificates.ts
Normal file
26
backend/src/db/schemas/ssh-certificates.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 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 SshCertificatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCaId: z.string().uuid(),
|
||||||
|
sshCertificateTemplateId: z.string().uuid().nullable().optional(),
|
||||||
|
serialNumber: z.string(),
|
||||||
|
certType: z.string(),
|
||||||
|
principals: z.string().array(),
|
||||||
|
keyId: z.string(),
|
||||||
|
notBefore: z.date(),
|
||||||
|
notAfter: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
|
||||||
|
export type TSshCertificatesInsert = Omit<z.input<typeof SshCertificatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificatesUpdate = Partial<Omit<z.input<typeof SshCertificatesSchema>, TImmutableDBKeys>>;
|
@@ -22,9 +22,13 @@ import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-rou
|
|||||||
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
||||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||||
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
||||||
|
import { registerSecretRouter } from "./secret-router";
|
||||||
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
||||||
import { registerSecretVersionRouter } from "./secret-version-router";
|
import { registerSecretVersionRouter } from "./secret-version-router";
|
||||||
import { registerSnapshotRouter } from "./snapshot-router";
|
import { registerSnapshotRouter } from "./snapshot-router";
|
||||||
|
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
||||||
|
import { registerSshCertRouter } from "./ssh-certificate-router";
|
||||||
|
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
||||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||||
|
|
||||||
@@ -68,6 +72,15 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/pki" }
|
{ prefix: "/pki" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (sshRouter) => {
|
||||||
|
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
|
||||||
|
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
||||||
|
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
|
},
|
||||||
|
{ prefix: "/ssh" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (ssoRouter) => {
|
async (ssoRouter) => {
|
||||||
await ssoRouter.register(registerSamlRouter);
|
await ssoRouter.register(registerSamlRouter);
|
||||||
@@ -80,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
||||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||||
|
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||||
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
||||||
|
@@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
),
|
),
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().nullish(),
|
||||||
permissions: z.any().array()
|
permissions: z.any().array()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -95,7 +95,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().nullish(),
|
||||||
permissions: z.any().array().optional()
|
permissions: z.any().array().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@@ -39,7 +39,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -95,7 +95,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||||
.optional(),
|
.optional(),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@@ -84,7 +84,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
samlConfig.audience = `spn:${ssoConfig.issuer}`;
|
samlConfig.audience = `spn:${ssoConfig.issuer}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ssoConfig.authProvider === SamlProviders.GOOGLE_SAML) {
|
if (
|
||||||
|
ssoConfig.authProvider === SamlProviders.GOOGLE_SAML ||
|
||||||
|
ssoConfig.authProvider === SamlProviders.AUTH0_SAML
|
||||||
|
) {
|
||||||
samlConfig.wantAssertionsSigned = false;
|
samlConfig.wantAssertionsSigned = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +126,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
`email: ${email} firstName: ${profile.firstName as string}`
|
`email: ${email} firstName: ${profile.firstName as string}`
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error("Invalid saml request. Missing email or first name");
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Missing email or first name. Please double check your SAML attribute mapping for the selected provider."
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMetadata = Object.keys(profile.attributes || {})
|
const userMetadata = Object.keys(profile.attributes || {})
|
||||||
|
@@ -12,6 +12,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
|
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
|
||||||
UsersSchema.pick({
|
UsersSchema.pick({
|
||||||
@@ -274,6 +275,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
.extend({
|
.extend({
|
||||||
op: z.string(),
|
op: z.string(),
|
||||||
tags: tagSchema,
|
tags: tagSchema,
|
||||||
|
secretMetadata: ResourceMetadataSchema.nullish(),
|
||||||
secret: z
|
secret: z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -291,7 +293,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
secretKey: z.string(),
|
secretKey: z.string(),
|
||||||
secretValue: z.string().optional(),
|
secretValue: z.string().optional(),
|
||||||
secretComment: z.string().optional(),
|
secretComment: z.string().optional(),
|
||||||
tags: tagSchema
|
tags: tagSchema,
|
||||||
|
secretMetadata: ResourceMetadataSchema.nullish()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
71
backend/src/ee/routes/v1/secret-router.ts
Normal file
71
backend/src/ee/routes/v1/secret-router.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { RAW_SECRETS } from "@app/lib/api-docs";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const AccessListEntrySchema = z
|
||||||
|
.object({
|
||||||
|
allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
|
||||||
|
id: z.string(),
|
||||||
|
membershipId: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
.array();
|
||||||
|
|
||||||
|
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:secretName/access-list",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get list of users, machine identities, and groups with access to a secret",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.workspaceId),
|
||||||
|
environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(RAW_SECRETS.GET_ACCESS_LIST.secretPath)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groups: AccessListEntrySchema,
|
||||||
|
identities: AccessListEntrySchema,
|
||||||
|
users: AccessListEntrySchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const { secretPath, environment, workspaceId: projectId } = req.query;
|
||||||
|
|
||||||
|
return server.services.secret.getSecretAccessList({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
secretName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
279
backend/src/ee/routes/v1/ssh-certificate-authority-router.ts
Normal file
279
backend/src/ee/routes/v1/ssh-certificate-authority-router.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
|
import { SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Create SSH CA",
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
||||||
|
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa.extend({
|
||||||
|
publicKey: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.createSshCa({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa.extend({
|
||||||
|
publicKey: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.getSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId/public-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get public key of SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_PUBLIC_KEY.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const publicKey = await server.services.sshCertificateAuthority.getSshCaPublicKey({
|
||||||
|
caId: req.params.sshCaId
|
||||||
|
});
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.sshCaId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
friendlyName: z.string().optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.friendlyName),
|
||||||
|
status: z.nativeEnum(SshCaStatus).optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa.extend({
|
||||||
|
publicKey: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.updateSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName,
|
||||||
|
status: ca.status as SshCaStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Delete SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.DELETE.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.deleteSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId/certificate-templates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get list of certificate templates for the SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_CERTIFICATE_TEMPLATES.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificateTemplates: sanitizedSshCertificateTemplate.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplates, ca } = await server.services.sshCertificateAuthority.getSshCaCertificateTemplates({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplates
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
164
backend/src/ee/routes/v1/ssh-certificate-router.ts
Normal file
164
backend/src/ee/routes/v1/ssh-certificate-router.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
|
import { 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";
|
||||||
|
|
||||||
|
export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/sign",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Sign SSH public key",
|
||||||
|
body: z.object({
|
||||||
|
certificateTemplateId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certificateTemplateId),
|
||||||
|
publicKey: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.publicKey),
|
||||||
|
certType: z
|
||||||
|
.nativeEnum(SshCertType)
|
||||||
|
.default(SshCertType.USER)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certType),
|
||||||
|
principals: z
|
||||||
|
.array(z.string().transform((val) => val.trim()))
|
||||||
|
.nonempty("Principals array must not be empty")
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.principals),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.ttl),
|
||||||
|
keyId: z.string().trim().max(50).optional().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.serialNumber),
|
||||||
|
signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.signedKey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { serialNumber, signedPublicKey, certificateTemplate, ttl, keyId } =
|
||||||
|
await server.services.sshCertificateAuthority.signSshKey({
|
||||||
|
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.SIGN_SSH_KEY,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
certType: req.body.certType,
|
||||||
|
principals: req.body.principals,
|
||||||
|
ttl: String(ttl),
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/issue",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Issue SSH credentials (certificate + key)",
|
||||||
|
body: z.object({
|
||||||
|
certificateTemplateId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
|
||||||
|
certType: z
|
||||||
|
.nativeEnum(SshCertType)
|
||||||
|
.default(SshCertType.USER)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certType),
|
||||||
|
principals: z
|
||||||
|
.array(z.string().transform((val) => val.trim()))
|
||||||
|
.nonempty("Principals array must not be empty")
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.principals),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.ttl),
|
||||||
|
keyId: z.string().trim().max(50).optional().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.serialNumber),
|
||||||
|
signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.signedKey),
|
||||||
|
privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey),
|
||||||
|
publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { serialNumber, signedPublicKey, privateKey, publicKey, certificateTemplate, ttl, keyId } =
|
||||||
|
await server.services.sshCertificateAuthority.issueSshCreds({
|
||||||
|
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.ISSUE_SSH_CREDS,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm,
|
||||||
|
certType: req.body.certType,
|
||||||
|
principals: req.body.principals,
|
||||||
|
ttl: String(ttl),
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
258
backend/src/ee/routes/v1/ssh-certificate-template-router.ts
Normal file
258
backend/src/ee/routes/v1/ssh-certificate-template-router.ts
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
|
import {
|
||||||
|
isValidHostPattern,
|
||||||
|
isValidUserPattern
|
||||||
|
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
|
import { SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSshCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.GET.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const certificateTemplate = await server.services.sshCertificateTemplate.getSshCertTemplate({
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: certificateTemplate.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
sshCaId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.sshCaId),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(36)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Name must be a valid slug"
|
||||||
|
})
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.name),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.default("1h")
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.ttl),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "Max TTL must be a positive number")
|
||||||
|
.default("30d")
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.maxTTL),
|
||||||
|
allowedUsers: z
|
||||||
|
.array(z.string().refine(isValidUserPattern, "Invalid user pattern"))
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedUsers),
|
||||||
|
allowedHosts: z
|
||||||
|
.array(z.string().refine(isValidHostPattern, "Invalid host pattern"))
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedHosts),
|
||||||
|
allowUserCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowUserCertificates),
|
||||||
|
allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates),
|
||||||
|
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
||||||
|
})
|
||||||
|
.refine((data) => ms(data.maxTTL) > ms(data.ttl), {
|
||||||
|
message: "Max TLL must be greater than TTL",
|
||||||
|
path: ["maxTTL"]
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplate, ca } = await server.services.sshCertificateTemplate.createSshCertTemplate({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
sshCaId: ca.id,
|
||||||
|
name: certificateTemplate.name,
|
||||||
|
ttl: certificateTemplate.ttl,
|
||||||
|
maxTTL: certificateTemplate.maxTTL,
|
||||||
|
allowedUsers: certificateTemplate.allowedUsers,
|
||||||
|
allowedHosts: certificateTemplate.allowedHosts,
|
||||||
|
allowUserCertificates: certificateTemplate.allowUserCertificates,
|
||||||
|
allowHostCertificates: certificateTemplate.allowHostCertificates,
|
||||||
|
allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
status: z.nativeEnum(SshCertTemplateStatus).optional(),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(36)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Slug must be a valid slug"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.name),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.ttl),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "Max TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.maxTTL),
|
||||||
|
allowedUsers: z
|
||||||
|
.array(z.string().refine(isValidUserPattern, "Invalid user pattern"))
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedUsers),
|
||||||
|
allowedHosts: z
|
||||||
|
.array(z.string().refine(isValidHostPattern, "Invalid host pattern"))
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedHosts),
|
||||||
|
allowUserCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowUserCertificates),
|
||||||
|
allowHostCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowHostCertificates),
|
||||||
|
allowCustomKeyIds: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowCustomKeyIds)
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplate, projectId } = await server.services.sshCertificateTemplate.updateSshCertTemplate({
|
||||||
|
...req.body,
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
status: certificateTemplate.status as SshCertTemplateStatus,
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
sshCaId: certificateTemplate.sshCaId,
|
||||||
|
name: certificateTemplate.name,
|
||||||
|
ttl: certificateTemplate.ttl,
|
||||||
|
maxTTL: certificateTemplate.maxTTL,
|
||||||
|
allowedUsers: certificateTemplate.allowedUsers,
|
||||||
|
allowedHosts: certificateTemplate.allowedHosts,
|
||||||
|
allowUserCertificates: certificateTemplate.allowUserCertificates,
|
||||||
|
allowHostCertificates: certificateTemplate.allowHostCertificates,
|
||||||
|
allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const certificateTemplate = await server.services.sshCertificateTemplate.deleteSshCertTemplate({
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: certificateTemplate.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -36,7 +36,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -91,7 +91,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@@ -213,7 +213,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||||
const approvalUrl = `${cfg.SITE_URL}/project/${project.id}/approval`;
|
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
|
||||||
|
|
||||||
await triggerSlackNotification({
|
await triggerSlackNotification({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
@@ -2,9 +2,14 @@ import {
|
|||||||
TCreateProjectTemplateDTO,
|
TCreateProjectTemplateDTO,
|
||||||
TUpdateProjectTemplateDTO
|
TUpdateProjectTemplateDTO
|
||||||
} from "@app/ee/services/project-template/project-template-types";
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||||
@@ -143,6 +148,17 @@ 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",
|
||||||
|
SIGN_SSH_KEY = "sign-ssh-key",
|
||||||
|
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
||||||
|
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
||||||
|
GET_SSH_CA = "get-ssh-certificate-authority",
|
||||||
|
UPDATE_SSH_CA = "update-ssh-certificate-authority",
|
||||||
|
DELETE_SSH_CA = "delete-ssh-certificate-authority",
|
||||||
|
GET_SSH_CA_CERTIFICATE_TEMPLATES = "get-ssh-certificate-authority-certificate-templates",
|
||||||
|
CREATE_SSH_CERTIFICATE_TEMPLATE = "create-ssh-certificate-template",
|
||||||
|
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||||
|
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||||
|
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||||
CREATE_CA = "create-certificate-authority",
|
CREATE_CA = "create-certificate-authority",
|
||||||
GET_CA = "get-certificate-authority",
|
GET_CA = "get-certificate-authority",
|
||||||
UPDATE_CA = "update-certificate-authority",
|
UPDATE_CA = "update-certificate-authority",
|
||||||
@@ -208,7 +224,12 @@ export enum EventType {
|
|||||||
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
||||||
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
||||||
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
||||||
APPLY_PROJECT_TEMPLATE = "apply-project-template"
|
APPLY_PROJECT_TEMPLATE = "apply-project-template",
|
||||||
|
GET_APP_CONNECTIONS = "get-app-connections",
|
||||||
|
GET_APP_CONNECTION = "get-app-connection",
|
||||||
|
CREATE_APP_CONNECTION = "create-app-connection",
|
||||||
|
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||||
|
DELETE_APP_CONNECTION = "delete-app-connection"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@@ -1206,6 +1227,117 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SignSshKey {
|
||||||
|
type: EventType.SIGN_SSH_KEY;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueSshCreds {
|
||||||
|
type: EventType.ISSUE_SSH_CREDS;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSshCa {
|
||||||
|
type: EventType.CREATE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCa {
|
||||||
|
type: EventType.GET_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSshCa {
|
||||||
|
type: EventType.UPDATE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
status: SshCaStatus;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshCa {
|
||||||
|
type: EventType.DELETE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCaCertificateTemplates {
|
||||||
|
type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSshCertificateTemplate {
|
||||||
|
type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCertificateTemplate {
|
||||||
|
type: EventType.GET_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSshCertificateTemplate {
|
||||||
|
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
status: SshCertTemplateStatus;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshCertificateTemplate {
|
||||||
|
type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCa {
|
interface CreateCa {
|
||||||
type: EventType.CREATE_CA;
|
type: EventType.CREATE_CA;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1742,6 +1874,39 @@ interface ApplyProjectTemplateEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetAppConnectionsEvent {
|
||||||
|
type: EventType.GET_APP_CONNECTIONS;
|
||||||
|
metadata: {
|
||||||
|
app?: AppConnection;
|
||||||
|
count: number;
|
||||||
|
connectionIds: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAppConnectionEvent {
|
||||||
|
type: EventType.GET_APP_CONNECTION;
|
||||||
|
metadata: {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateAppConnectionEvent {
|
||||||
|
type: EventType.CREATE_APP_CONNECTION;
|
||||||
|
metadata: Omit<TCreateAppConnectionDTO, "credentials"> & { connectionId: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateAppConnectionEvent {
|
||||||
|
type: EventType.UPDATE_APP_CONNECTION;
|
||||||
|
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteAppConnectionEvent {
|
||||||
|
type: EventType.DELETE_APP_CONNECTION;
|
||||||
|
metadata: {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -1837,6 +2002,17 @@ export type Event =
|
|||||||
| SecretApprovalClosed
|
| SecretApprovalClosed
|
||||||
| SecretApprovalRequest
|
| SecretApprovalRequest
|
||||||
| SecretApprovalReopened
|
| SecretApprovalReopened
|
||||||
|
| SignSshKey
|
||||||
|
| IssueSshCreds
|
||||||
|
| CreateSshCa
|
||||||
|
| GetSshCa
|
||||||
|
| UpdateSshCa
|
||||||
|
| DeleteSshCa
|
||||||
|
| GetSshCaCertificateTemplates
|
||||||
|
| CreateSshCertificateTemplate
|
||||||
|
| UpdateSshCertificateTemplate
|
||||||
|
| GetSshCertificateTemplate
|
||||||
|
| DeleteSshCertificateTemplate
|
||||||
| CreateCa
|
| CreateCa
|
||||||
| GetCa
|
| GetCa
|
||||||
| UpdateCa
|
| UpdateCa
|
||||||
@@ -1902,4 +2078,9 @@ export type Event =
|
|||||||
| CreateProjectTemplateEvent
|
| CreateProjectTemplateEvent
|
||||||
| UpdateProjectTemplateEvent
|
| UpdateProjectTemplateEvent
|
||||||
| DeleteProjectTemplateEvent
|
| DeleteProjectTemplateEvent
|
||||||
| ApplyProjectTemplateEvent;
|
| ApplyProjectTemplateEvent
|
||||||
|
| GetAppConnectionsEvent
|
||||||
|
| GetAppConnectionEvent
|
||||||
|
| CreateAppConnectionEvent
|
||||||
|
| UpdateAppConnectionEvent
|
||||||
|
| DeleteAppConnectionEvent;
|
||||||
|
@@ -34,6 +34,8 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||||
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||||
|
const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL;
|
||||||
|
|
||||||
const db = knex({
|
const db = knex({
|
||||||
client: providerInputs.client,
|
client: providerInputs.client,
|
||||||
connection: {
|
connection: {
|
||||||
@@ -43,7 +45,16 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
user: providerInputs.username,
|
user: providerInputs.username,
|
||||||
password: providerInputs.password,
|
password: providerInputs.password,
|
||||||
ssl,
|
ssl,
|
||||||
pool: { min: 0, max: 1 }
|
pool: { min: 0, max: 1 },
|
||||||
|
// @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/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||||
|
options: isMsSQLClient
|
||||||
|
? {
|
||||||
|
trustServerCertificate: !providerInputs.ca,
|
||||||
|
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
|
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
});
|
});
|
||||||
|
@@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
rbac: false,
|
rbac: false,
|
||||||
customRateLimits: false,
|
customRateLimits: false,
|
||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
|
secretAccessInsights: false,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
auditLogStreams: false,
|
auditLogStreams: false,
|
||||||
@@ -49,7 +50,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
},
|
},
|
||||||
pkiEst: false,
|
pkiEst: false,
|
||||||
enforceMfa: false,
|
enforceMfa: false,
|
||||||
projectTemplates: false
|
projectTemplates: false,
|
||||||
|
appConnections: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@@ -246,8 +246,7 @@ export const licenseServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
|
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
|
||||||
const plan = await getPlan(orgId, projectId);
|
const plan = await getPlan(orgId, projectId);
|
||||||
return plan;
|
return plan;
|
||||||
};
|
};
|
||||||
|
@@ -48,6 +48,7 @@ export type TFeatureSet = {
|
|||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
hsm: false;
|
hsm: false;
|
||||||
oidcSSO: false;
|
oidcSSO: false;
|
||||||
|
secretAccessInsights: false;
|
||||||
scim: false;
|
scim: false;
|
||||||
ldap: false;
|
ldap: false;
|
||||||
groups: false;
|
groups: false;
|
||||||
@@ -67,6 +68,7 @@ export type TFeatureSet = {
|
|||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
projectTemplates: false;
|
projectTemplates: false;
|
||||||
|
appConnections: false; // TODO: remove once live
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@@ -27,7 +27,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console",
|
AdminConsole = "organization-admin-console",
|
||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
ProjectTemplates = "project-templates"
|
ProjectTemplates = "project-templates",
|
||||||
|
AppConnections = "app-connections"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -46,6 +47,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
@@ -123,6 +125,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
@@ -153,6 +160,8 @@ const buildMemberPermission = () => {
|
|||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -125,6 +125,404 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectGroupPermissions = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const docs = await db
|
||||||
|
.replicaNode()(TableName.GroupProjectMembership)
|
||||||
|
.join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembershipRole,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.GroupProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin<TProjectRoles>(
|
||||||
|
{ groupCustomRoles: TableName.ProjectRoles },
|
||||||
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
|
`groupCustomRoles.id`
|
||||||
|
)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
||||||
|
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||||
|
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||||
|
db.ref("slug").withSchema("groupCustomRoles").as("groupProjectMembershipRoleCustomRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema("groupCustomRoles").as("groupProjectMembershipRolePermission"),
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRole"),
|
||||||
|
db
|
||||||
|
.ref("customRoleId")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleCustomRoleId"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleTemporaryAccessEndTime")
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupPermissions = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "groupId",
|
||||||
|
parentMapper: ({ groupId, groupName, membershipId }) => ({
|
||||||
|
groupId,
|
||||||
|
username: groupName,
|
||||||
|
id: membershipId
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "groupProjectMembershipRoleId",
|
||||||
|
label: "groupRoles" as const,
|
||||||
|
mapper: ({
|
||||||
|
groupProjectMembershipRoleId,
|
||||||
|
groupProjectMembershipRole,
|
||||||
|
groupProjectMembershipRolePermission,
|
||||||
|
groupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
groupProjectMembershipRoleIsTemporary,
|
||||||
|
groupProjectMembershipRoleTemporaryMode,
|
||||||
|
groupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
groupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
groupProjectMembershipRoleTemporaryRange
|
||||||
|
}) => ({
|
||||||
|
id: groupProjectMembershipRoleId,
|
||||||
|
role: groupProjectMembershipRole,
|
||||||
|
customRoleSlug: groupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: groupProjectMembershipRolePermission,
|
||||||
|
temporaryRange: groupProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: groupProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: groupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: groupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: groupProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupPermissions
|
||||||
|
.map((groupPermission) => {
|
||||||
|
if (!groupPermission) return undefined;
|
||||||
|
|
||||||
|
const activeGroupRoles =
|
||||||
|
groupPermission?.groupRoles?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...groupPermission,
|
||||||
|
roles: activeGroupRoles
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetProjectGroupPermissions" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectUserPermissions = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const docs = await db
|
||||||
|
.replicaNode()(TableName.Users)
|
||||||
|
.where("isGhost", "=", false)
|
||||||
|
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
||||||
|
void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]));
|
||||||
|
})
|
||||||
|
.leftJoin(
|
||||||
|
TableName.GroupProjectMembershipRole,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.GroupProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin<TProjectRoles>(
|
||||||
|
{ groupCustomRoles: TableName.ProjectRoles },
|
||||||
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
|
`groupCustomRoles.id`
|
||||||
|
)
|
||||||
|
.join(TableName.ProjectMembership, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||||
|
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
|
||||||
|
})
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectUserMembershipRole,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.ProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId]))
|
||||||
|
.andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`);
|
||||||
|
})
|
||||||
|
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||||
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
||||||
|
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("username").withSchema(TableName.Users).as("username"),
|
||||||
|
// groups specific
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"),
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"),
|
||||||
|
db
|
||||||
|
.ref("customRoleId")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleCustomRoleId"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryAccessEndTime"),
|
||||||
|
// user specific
|
||||||
|
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"),
|
||||||
|
db
|
||||||
|
.ref("permissions")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesPermissions"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryRange"),
|
||||||
|
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||||
|
// general
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||||
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
|
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||||
|
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||||
|
);
|
||||||
|
|
||||||
|
const userPermissions = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "userId",
|
||||||
|
parentMapper: ({
|
||||||
|
orgId,
|
||||||
|
username,
|
||||||
|
orgAuthEnforced,
|
||||||
|
membershipId,
|
||||||
|
groupMembershipId,
|
||||||
|
membershipCreatedAt,
|
||||||
|
groupMembershipCreatedAt,
|
||||||
|
groupMembershipUpdatedAt,
|
||||||
|
membershipUpdatedAt,
|
||||||
|
projectType,
|
||||||
|
userId
|
||||||
|
}) => ({
|
||||||
|
orgId,
|
||||||
|
orgAuthEnforced,
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
username,
|
||||||
|
projectType,
|
||||||
|
id: membershipId || groupMembershipId,
|
||||||
|
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||||
|
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "userGroupProjectMembershipRoleId",
|
||||||
|
label: "userGroupRoles" as const,
|
||||||
|
mapper: ({
|
||||||
|
userGroupProjectMembershipRoleId,
|
||||||
|
userGroupProjectMembershipRole,
|
||||||
|
userGroupProjectMembershipRolePermission,
|
||||||
|
userGroupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
userGroupProjectMembershipRoleIsTemporary,
|
||||||
|
userGroupProjectMembershipRoleTemporaryMode,
|
||||||
|
userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
userGroupProjectMembershipRoleTemporaryRange
|
||||||
|
}) => ({
|
||||||
|
id: userGroupProjectMembershipRoleId,
|
||||||
|
role: userGroupProjectMembershipRole,
|
||||||
|
customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: userGroupProjectMembershipRolePermission,
|
||||||
|
temporaryRange: userGroupProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: userGroupProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: userGroupProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userProjectMembershipRoleId",
|
||||||
|
label: "projectMembershipRoles" as const,
|
||||||
|
mapper: ({
|
||||||
|
userProjectMembershipRoleId,
|
||||||
|
userProjectMembershipRole,
|
||||||
|
userProjectCustomRolePermission,
|
||||||
|
userProjectMembershipRoleIsTemporary,
|
||||||
|
userProjectMembershipRoleTemporaryMode,
|
||||||
|
userProjectMembershipRoleTemporaryRange,
|
||||||
|
userProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
userProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
userProjectMembershipRoleCustomRoleSlug
|
||||||
|
}) => ({
|
||||||
|
id: userProjectMembershipRoleId,
|
||||||
|
role: userProjectMembershipRole,
|
||||||
|
customRoleSlug: userProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: userProjectCustomRolePermission,
|
||||||
|
temporaryRange: userProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: userProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: userProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userAdditionalPrivilegesId",
|
||||||
|
label: "additionalPrivileges" as const,
|
||||||
|
mapper: ({
|
||||||
|
userAdditionalPrivilegesId,
|
||||||
|
userAdditionalPrivilegesPermissions,
|
||||||
|
userAdditionalPrivilegesIsTemporary,
|
||||||
|
userAdditionalPrivilegesTemporaryMode,
|
||||||
|
userAdditionalPrivilegesTemporaryRange,
|
||||||
|
userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
|
userAdditionalPrivilegesTemporaryAccessStartTime
|
||||||
|
}) => ({
|
||||||
|
id: userAdditionalPrivilegesId,
|
||||||
|
permissions: userAdditionalPrivilegesPermissions,
|
||||||
|
temporaryRange: userAdditionalPrivilegesTemporaryRange,
|
||||||
|
temporaryMode: userAdditionalPrivilegesTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
|
isTemporary: userAdditionalPrivilegesIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return userPermissions
|
||||||
|
.map((userPermission) => {
|
||||||
|
if (!userPermission) return undefined;
|
||||||
|
if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projectMembershipRoles?.[0]) return undefined;
|
||||||
|
|
||||||
|
// when introducting cron mode change it here
|
||||||
|
const activeRoles =
|
||||||
|
userPermission?.projectMembershipRoles?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const activeGroupRoles =
|
||||||
|
userPermission?.userGroupRoles?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const activeAdditionalPrivileges =
|
||||||
|
userPermission?.additionalPrivileges?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...userPermission,
|
||||||
|
roles: [...activeRoles, ...activeGroupRoles],
|
||||||
|
additionalPrivileges: activeAdditionalPrivileges
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetProjectUserPermissions" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
|
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
|
||||||
@@ -414,6 +812,163 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectIdentityPermissions = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const docs = await db
|
||||||
|
.replicaNode()(TableName.IdentityProjectMembership)
|
||||||
|
.join(
|
||||||
|
TableName.IdentityProjectMembershipRole,
|
||||||
|
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityProjectAdditionalPrivilege,
|
||||||
|
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`,
|
||||||
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
// Join the Project table to later select orgId
|
||||||
|
TableName.Project,
|
||||||
|
`${TableName.IdentityProjectMembership}.projectId`,
|
||||||
|
`${TableName.Project}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
|
||||||
|
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
|
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||||
|
db.ref("id").withSchema(TableName.Identity).as("identityId"),
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||||
|
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
|
||||||
|
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
|
.as("identityApTemporaryMode"),
|
||||||
|
db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
|
.as("identityApTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
|
.as("identityApTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
|
.as("identityApTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
|
);
|
||||||
|
|
||||||
|
const permissions = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "identityId",
|
||||||
|
parentMapper: ({
|
||||||
|
membershipId,
|
||||||
|
membershipCreatedAt,
|
||||||
|
membershipUpdatedAt,
|
||||||
|
orgId,
|
||||||
|
identityName,
|
||||||
|
projectType,
|
||||||
|
identityId
|
||||||
|
}) => ({
|
||||||
|
id: membershipId,
|
||||||
|
identityId,
|
||||||
|
username: identityName,
|
||||||
|
projectId,
|
||||||
|
createdAt: membershipCreatedAt,
|
||||||
|
updatedAt: membershipUpdatedAt,
|
||||||
|
orgId,
|
||||||
|
projectType,
|
||||||
|
// just a prefilled value
|
||||||
|
orgAuthEnforced: false
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "id",
|
||||||
|
label: "roles" as const,
|
||||||
|
mapper: (data) =>
|
||||||
|
IdentityProjectMembershipRoleSchema.extend({
|
||||||
|
permissions: z.unknown(),
|
||||||
|
customRoleSlug: z.string().optional().nullable()
|
||||||
|
}).parse(data)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "identityApId",
|
||||||
|
label: "additionalPrivileges" as const,
|
||||||
|
mapper: ({
|
||||||
|
identityApId,
|
||||||
|
identityApPermissions,
|
||||||
|
identityApIsTemporary,
|
||||||
|
identityApTemporaryMode,
|
||||||
|
identityApTemporaryRange,
|
||||||
|
identityApTemporaryAccessEndTime,
|
||||||
|
identityApTemporaryAccessStartTime
|
||||||
|
}) => ({
|
||||||
|
id: identityApId,
|
||||||
|
permissions: identityApPermissions,
|
||||||
|
temporaryRange: identityApTemporaryRange,
|
||||||
|
temporaryMode: identityApTemporaryMode,
|
||||||
|
temporaryAccessEndTime: identityApTemporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
||||||
|
isTemporary: identityApIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
.map((permission) => {
|
||||||
|
if (!permission) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when introducting cron mode change it here
|
||||||
|
const activeRoles = permission?.roles.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
);
|
||||||
|
const activeAdditionalPrivileges = permission?.additionalPrivileges?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...permission, roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
|
||||||
|
})
|
||||||
|
.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetProjectIdentityPermissions" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
|
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const docs = await db
|
const docs = await db
|
||||||
@@ -568,6 +1123,9 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
getOrgPermission,
|
getOrgPermission,
|
||||||
getOrgIdentityPermission,
|
getOrgIdentityPermission,
|
||||||
getProjectPermission,
|
getProjectPermission,
|
||||||
getProjectIdentityPermission
|
getProjectIdentityPermission,
|
||||||
|
getProjectUserPermissions,
|
||||||
|
getProjectIdentityPermissions,
|
||||||
|
getProjectGroupPermissions
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -405,6 +405,123 @@ export const permissionServiceFactory = ({
|
|||||||
ForbidOnInvalidProjectType: (type: ProjectType) => void;
|
ForbidOnInvalidProjectType: (type: ProjectType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectPermissions = async (projectId: string) => {
|
||||||
|
// fetch user permissions
|
||||||
|
const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId);
|
||||||
|
const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => {
|
||||||
|
const rolePermissions =
|
||||||
|
userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
|
const additionalPrivileges =
|
||||||
|
userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
|
||||||
|
role: ProjectMembershipRole.Custom,
|
||||||
|
permissions
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
|
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||||
|
objectify(
|
||||||
|
userProjectPermission.metadata,
|
||||||
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
id: userProjectPermission.userId,
|
||||||
|
username: userProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission,
|
||||||
|
id: userProjectPermission.userId,
|
||||||
|
name: userProjectPermission.username,
|
||||||
|
membershipId: userProjectPermission.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch identity permissions
|
||||||
|
const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId);
|
||||||
|
const identityPermissions = rawIdentityProjectPermissions.map((identityProjectPermission) => {
|
||||||
|
const rolePermissions =
|
||||||
|
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
|
const additionalPrivileges =
|
||||||
|
identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
|
||||||
|
role: ProjectMembershipRole.Custom,
|
||||||
|
permissions
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
|
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||||
|
objectify(
|
||||||
|
identityProjectPermission.metadata,
|
||||||
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
id: identityProjectPermission.identityId,
|
||||||
|
username: identityProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission,
|
||||||
|
id: identityProjectPermission.identityId,
|
||||||
|
name: identityProjectPermission.username,
|
||||||
|
membershipId: identityProjectPermission.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch group permissions
|
||||||
|
const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId);
|
||||||
|
const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => {
|
||||||
|
const rolePermissions =
|
||||||
|
groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission,
|
||||||
|
id: groupProjectPermission.groupId,
|
||||||
|
name: groupProjectPermission.username,
|
||||||
|
membershipId: groupProjectPermission.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userPermissions,
|
||||||
|
identityPermissions,
|
||||||
|
groupPermissions
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getProjectPermission = async <T extends ActorType>(
|
const getProjectPermission = async <T extends ActorType>(
|
||||||
type: T,
|
type: T,
|
||||||
id: string,
|
id: string,
|
||||||
@@ -455,6 +572,7 @@ export const permissionServiceFactory = ({
|
|||||||
getOrgPermission,
|
getOrgPermission,
|
||||||
getUserProjectPermission,
|
getUserProjectPermission,
|
||||||
getProjectPermission,
|
getProjectPermission,
|
||||||
|
getProjectPermissions,
|
||||||
getOrgPermissionByRole,
|
getOrgPermissionByRole,
|
||||||
getProjectPermissionByRole,
|
getProjectPermissionByRole,
|
||||||
buildOrgPermission,
|
buildOrgPermission,
|
||||||
|
@@ -54,6 +54,9 @@ export enum ProjectPermissionSub {
|
|||||||
CertificateAuthorities = "certificate-authorities",
|
CertificateAuthorities = "certificate-authorities",
|
||||||
Certificates = "certificates",
|
Certificates = "certificates",
|
||||||
CertificateTemplates = "certificate-templates",
|
CertificateTemplates = "certificate-templates",
|
||||||
|
SshCertificateAuthorities = "ssh-certificate-authorities",
|
||||||
|
SshCertificates = "ssh-certificates",
|
||||||
|
SshCertificateTemplates = "ssh-certificate-templates",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
@@ -132,6 +135,9 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||||
@@ -338,6 +344,28 @@ const GeneralPermissionSchema = [
|
|||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z
|
||||||
|
.literal(ProjectPermissionSub.SshCertificateAuthorities)
|
||||||
|
.describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SshCertificates).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z
|
||||||
|
.literal(ProjectPermissionSub.SshCertificateTemplates)
|
||||||
|
.describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
@@ -480,7 +508,10 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.Certificates,
|
ProjectPermissionSub.Certificates,
|
||||||
ProjectPermissionSub.CertificateTemplates,
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
ProjectPermissionSub.PkiAlerts,
|
ProjectPermissionSub.PkiAlerts,
|
||||||
ProjectPermissionSub.PkiCollections
|
ProjectPermissionSub.PkiCollections,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities,
|
||||||
|
ProjectPermissionSub.SshCertificates,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
].forEach((el) => {
|
].forEach((el) => {
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
@@ -665,6 +696,11 @@ const buildMemberPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||||
|
|
||||||
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
|
||||||
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||||
|
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||||
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionCmekActions.Create,
|
ProjectPermissionCmekActions.Create,
|
||||||
@@ -707,6 +743,9 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
@@ -6,7 +6,8 @@ export enum SamlProviders {
|
|||||||
AZURE_SAML = "azure-saml",
|
AZURE_SAML = "azure-saml",
|
||||||
JUMPCLOUD_SAML = "jumpcloud-saml",
|
JUMPCLOUD_SAML = "jumpcloud-saml",
|
||||||
GOOGLE_SAML = "google-saml",
|
GOOGLE_SAML = "google-saml",
|
||||||
KEYCLOAK_SAML = "keycloak-saml"
|
KEYCLOAK_SAML = "keycloak-saml",
|
||||||
|
AUTH0_SAML = "auth0-saml"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCreateSamlCfgDTO = {
|
export type TCreateSamlCfgDTO = {
|
||||||
|
@@ -36,7 +36,7 @@ export const sendApprovalEmailsFn = async ({
|
|||||||
firstName: reviewerUser.firstName,
|
firstName: reviewerUser.firstName,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
organizationName: project.organization.name,
|
organizationName: project.organization.name,
|
||||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.SecretApprovalRequestNeedsReview
|
template: SmtpTemplates.SecretApprovalRequestNeedsReview
|
||||||
});
|
});
|
||||||
|
@@ -256,6 +256,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||||
db.ref("id").withSchema("secVerTag")
|
db.ref("id").withSchema("secVerTag")
|
||||||
)
|
)
|
||||||
|
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
|
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
|
||||||
.select({
|
.select({
|
||||||
secVerTagId: "secVerTag.id",
|
secVerTagId: "secVerTag.id",
|
||||||
@@ -279,6 +280,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
|
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
|
||||||
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
|
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
|
||||||
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
|
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
);
|
);
|
||||||
const formatedDoc = sqlNestRelationships({
|
const formatedDoc = sqlNestRelationships({
|
||||||
data: doc,
|
data: doc,
|
||||||
@@ -338,9 +344,19 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "oldSecretMetadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
|
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
secret: secret?.[0],
|
secret: secret?.[0],
|
||||||
|
@@ -22,6 +22,8 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
|
|||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
|
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||||
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
import {
|
import {
|
||||||
decryptSecretWithBot,
|
decryptSecretWithBot,
|
||||||
@@ -91,6 +93,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
|
||||||
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">;
|
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">;
|
||||||
@@ -138,7 +141,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectSlackConfigDAL
|
projectSlackConfigDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
@@ -241,6 +245,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretKey: el.key,
|
secretKey: el.key,
|
||||||
id: el.id,
|
id: el.id,
|
||||||
version: el.version,
|
version: el.version,
|
||||||
|
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||||
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||||
secretComment: el.encryptedComment
|
secretComment: el.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||||
@@ -269,7 +274,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretComment: el.secretVersion.encryptedComment
|
secretComment: el.secretVersion.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||||
: "",
|
: "",
|
||||||
tags: el.secretVersion.tags
|
tags: el.secretVersion.tags,
|
||||||
|
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}));
|
}));
|
||||||
@@ -543,6 +549,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
? await fnSecretV2BridgeBulkInsert({
|
? await fnSecretV2BridgeBulkInsert({
|
||||||
tx,
|
tx,
|
||||||
folderId,
|
folderId,
|
||||||
|
orgId: actorOrgId,
|
||||||
inputSecrets: secretCreationCommits.map((el) => ({
|
inputSecrets: secretCreationCommits.map((el) => ({
|
||||||
tagIds: el?.tags.map(({ id }) => id),
|
tagIds: el?.tags.map(({ id }) => id),
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -550,6 +557,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
encryptedValue: el.encryptedValue,
|
encryptedValue: el.encryptedValue,
|
||||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
key: el.key,
|
key: el.key,
|
||||||
|
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||||
references: el.encryptedValue
|
references: el.encryptedValue
|
||||||
? getAllSecretReferencesV2Bridge(
|
? getAllSecretReferencesV2Bridge(
|
||||||
secretManagerDecryptor({
|
secretManagerDecryptor({
|
||||||
@@ -559,6 +567,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
: [],
|
: [],
|
||||||
type: SecretType.Shared
|
type: SecretType.Shared
|
||||||
})),
|
})),
|
||||||
|
resourceMetadataDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
@@ -568,6 +577,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const updatedSecrets = secretUpdationCommits.length
|
const updatedSecrets = secretUpdationCommits.length
|
||||||
? await fnSecretV2BridgeBulkUpdate({
|
? await fnSecretV2BridgeBulkUpdate({
|
||||||
folderId,
|
folderId,
|
||||||
|
orgId: actorOrgId,
|
||||||
tx,
|
tx,
|
||||||
inputSecrets: secretUpdationCommits.map((el) => {
|
inputSecrets: secretUpdationCommits.map((el) => {
|
||||||
const encryptedValue =
|
const encryptedValue =
|
||||||
@@ -592,6 +602,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
key: el.key,
|
key: el.key,
|
||||||
tags: el?.tags.map(({ id }) => id),
|
tags: el?.tags.map(({ id }) => id),
|
||||||
|
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||||
...encryptedValue
|
...encryptedValue
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -599,7 +610,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL
|
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const deletedSecret = secretDeletionCommits.length
|
const deletedSecret = secretDeletionCommits.length
|
||||||
@@ -824,6 +836,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
}
|
}
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
projectId,
|
projectId,
|
||||||
|
orgId: actorOrgId,
|
||||||
secretPath: folder.path,
|
secretPath: folder.path,
|
||||||
environmentSlug: folder.environmentSlug,
|
environmentSlug: folder.environmentSlug,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -852,7 +865,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
bypassReason,
|
bypassReason,
|
||||||
secretPath: policy.secretPath,
|
secretPath: policy.secretPath,
|
||||||
environment: env.name,
|
environment: env.name,
|
||||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.AccessSecretRequestBypassed
|
template: SmtpTemplates.AccessSecretRequestBypassed
|
||||||
});
|
});
|
||||||
@@ -1208,6 +1221,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
),
|
),
|
||||||
skipMultilineEncoding: createdSecret.skipMultilineEncoding,
|
skipMultilineEncoding: createdSecret.skipMultilineEncoding,
|
||||||
key: createdSecret.secretKey,
|
key: createdSecret.secretKey,
|
||||||
|
secretMetadata: createdSecret.secretMetadata,
|
||||||
type: SecretType.Shared
|
type: SecretType.Shared
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -1263,12 +1277,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
reminderNote,
|
reminderNote,
|
||||||
secretComment,
|
secretComment,
|
||||||
metadata,
|
metadata,
|
||||||
skipMultilineEncoding
|
skipMultilineEncoding,
|
||||||
|
secretMetadata
|
||||||
}) => {
|
}) => {
|
||||||
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
||||||
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
|
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
|
||||||
return {
|
return {
|
||||||
...latestSecretVersions[secretId],
|
...latestSecretVersions[secretId],
|
||||||
|
secretMetadata,
|
||||||
key: newSecretName || secretKey,
|
key: newSecretName || secretKey,
|
||||||
encryptedComment: setKnexStringValue(
|
encryptedComment: setKnexStringValue(
|
||||||
secretComment,
|
secretComment,
|
||||||
@@ -1370,7 +1386,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
reminderRepeatDays,
|
reminderRepeatDays,
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
secretId,
|
secretId,
|
||||||
secretVersion
|
secretVersion,
|
||||||
|
secretMetadata
|
||||||
}) => ({
|
}) => ({
|
||||||
version,
|
version,
|
||||||
requestId: doc.id,
|
requestId: doc.id,
|
||||||
@@ -1383,7 +1400,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
reminderRepeatDays,
|
reminderRepeatDays,
|
||||||
reminderNote,
|
reminderNote,
|
||||||
encryptedComment,
|
encryptedComment,
|
||||||
key
|
key,
|
||||||
|
secretMetadata: JSON.stringify(secretMetadata)
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
tx
|
tx
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas";
|
import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import { SecretOperations } from "@app/services/secret/secret-types";
|
import { SecretOperations } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
export enum RequestState {
|
export enum RequestState {
|
||||||
@@ -34,6 +35,7 @@ export type TApprovalCreateSecretV2Bridge = {
|
|||||||
reminderRepeatDays?: number | null;
|
reminderRepeatDays?: number | null;
|
||||||
skipMultilineEncoding?: boolean;
|
skipMultilineEncoding?: boolean;
|
||||||
metadata?: Record<string, string>;
|
metadata?: Record<string, string>;
|
||||||
|
secretMetadata?: ResourceMetadataDTO;
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -13,6 +13,8 @@ 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";
|
||||||
|
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||||
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
|
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
|
||||||
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
|
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
|
||||||
@@ -56,6 +58,7 @@ type TSecretReplicationServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
|
||||||
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "find" | "insertMany">;
|
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "find" | "insertMany">;
|
||||||
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
|
||||||
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">;
|
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">;
|
||||||
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
|
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
|
||||||
@@ -121,7 +124,8 @@ export const secretReplicationServiceFactory = ({
|
|||||||
secretVersionV2TagBridgeDAL,
|
secretVersionV2TagBridgeDAL,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
resourceMetadataDAL
|
||||||
}: TSecretReplicationServiceFactoryDep) => {
|
}: TSecretReplicationServiceFactoryDep) => {
|
||||||
const $getReplicatedSecrets = (
|
const $getReplicatedSecrets = (
|
||||||
botKey: string,
|
botKey: string,
|
||||||
@@ -151,8 +155,10 @@ export const secretReplicationServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const $getReplicatedSecretsV2 = (
|
const $getReplicatedSecretsV2 = (
|
||||||
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[],
|
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[],
|
||||||
importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[]
|
importedSecrets: {
|
||||||
|
secrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[];
|
||||||
|
}[]
|
||||||
) => {
|
) => {
|
||||||
const deDupe = new Set<string>();
|
const deDupe = new Set<string>();
|
||||||
const secrets = [...localSecrets];
|
const secrets = [...localSecrets];
|
||||||
@@ -178,6 +184,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
environmentSlug,
|
environmentSlug,
|
||||||
projectId,
|
projectId,
|
||||||
|
orgId,
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
pickOnlyImportIds,
|
pickOnlyImportIds,
|
||||||
@@ -222,6 +229,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
.map(({ folderId }) =>
|
.map(({ folderId }) =>
|
||||||
secretQueueService.replicateSecrets({
|
secretQueueService.replicateSecrets({
|
||||||
projectId,
|
projectId,
|
||||||
|
orgId,
|
||||||
secretPath: foldersGroupedById[folderId][0]?.path as string,
|
secretPath: foldersGroupedById[folderId][0]?.path as string,
|
||||||
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
|
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -267,6 +275,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||||
: undefined
|
: undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
|
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
|
||||||
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
|
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
|
||||||
|
|
||||||
@@ -333,13 +342,29 @@ export const secretReplicationServiceFactory = ({
|
|||||||
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
|
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
|
||||||
|
|
||||||
const locallyUpdatedSecrets = sourceSecrets
|
const locallyUpdatedSecrets = sourceSecrets
|
||||||
.filter(
|
.filter(({ key, secretKey, secretValue, secretMetadata }) => {
|
||||||
({ key, secretKey, secretValue }) =>
|
const sourceSecretMetadataJson = JSON.stringify(
|
||||||
|
(secretMetadata ?? []).map((entry) => ({
|
||||||
|
key: entry.key,
|
||||||
|
value: entry.value
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const destinationSecretMetadataJson = JSON.stringify(
|
||||||
|
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretMetadata ?? []).map((entry) => ({
|
||||||
|
key: entry.key,
|
||||||
|
value: entry.value
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
destinationLocalSecretsGroupedByKey[key]?.[0] &&
|
destinationLocalSecretsGroupedByKey[key]?.[0] &&
|
||||||
// if key or value changed
|
// if key or value changed
|
||||||
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey ||
|
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey ||
|
||||||
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue)
|
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue ||
|
||||||
)
|
sourceSecretMetadataJson !== destinationSecretMetadataJson)
|
||||||
|
);
|
||||||
|
})
|
||||||
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
|
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
|
||||||
|
|
||||||
const locallyDeletedSecrets = destinationLocalSecrets
|
const locallyDeletedSecrets = destinationLocalSecrets
|
||||||
@@ -387,6 +412,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
op: operation,
|
op: operation,
|
||||||
requestId: approvalRequestDoc.id,
|
requestId: approvalRequestDoc.id,
|
||||||
metadata: doc.metadata,
|
metadata: doc.metadata,
|
||||||
|
secretMetadata: JSON.stringify(doc.secretMetadata),
|
||||||
key: doc.key,
|
key: doc.key,
|
||||||
encryptedValue: doc.encryptedValue,
|
encryptedValue: doc.encryptedValue,
|
||||||
encryptedComment: doc.encryptedComment,
|
encryptedComment: doc.encryptedComment,
|
||||||
@@ -406,10 +432,12 @@ export const secretReplicationServiceFactory = ({
|
|||||||
if (locallyCreatedSecrets.length) {
|
if (locallyCreatedSecrets.length) {
|
||||||
await fnSecretV2BridgeBulkInsert({
|
await fnSecretV2BridgeBulkInsert({
|
||||||
folderId: destinationReplicationFolderId,
|
folderId: destinationReplicationFolderId,
|
||||||
|
orgId,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
|
resourceMetadataDAL,
|
||||||
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
||||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
@@ -419,6 +447,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
encryptedValue: doc.encryptedValue,
|
encryptedValue: doc.encryptedValue,
|
||||||
encryptedComment: doc.encryptedComment,
|
encryptedComment: doc.encryptedComment,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||||
|
secretMetadata: doc.secretMetadata,
|
||||||
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
|
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -426,10 +455,12 @@ export const secretReplicationServiceFactory = ({
|
|||||||
}
|
}
|
||||||
if (locallyUpdatedSecrets.length) {
|
if (locallyUpdatedSecrets.length) {
|
||||||
await fnSecretV2BridgeBulkUpdate({
|
await fnSecretV2BridgeBulkUpdate({
|
||||||
|
orgId,
|
||||||
folderId: destinationReplicationFolderId,
|
folderId: destinationReplicationFolderId,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
tx,
|
tx,
|
||||||
|
resourceMetadataDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
||||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||||
@@ -445,6 +476,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
encryptedValue: doc.encryptedValue as Buffer,
|
encryptedValue: doc.encryptedValue as Buffer,
|
||||||
encryptedComment: doc.encryptedComment,
|
encryptedComment: doc.encryptedComment,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||||
|
secretMetadata: doc.secretMetadata,
|
||||||
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
|
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -466,6 +498,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
|
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
projectId,
|
projectId,
|
||||||
|
orgId,
|
||||||
secretPath: destinationFolder.path,
|
secretPath: destinationFolder.path,
|
||||||
environmentSlug: destinationFolder.environmentSlug,
|
environmentSlug: destinationFolder.environmentSlug,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -751,6 +784,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
|
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
projectId,
|
projectId,
|
||||||
|
orgId,
|
||||||
secretPath: destinationFolder.path,
|
secretPath: destinationFolder.path,
|
||||||
environmentSlug: destinationFolder.environmentSlug,
|
environmentSlug: destinationFolder.environmentSlug,
|
||||||
actorId,
|
actorId,
|
||||||
|
@@ -180,6 +180,8 @@ export const secretRotationQueueFactory = ({
|
|||||||
provider.template.client === TDbProviderClients.MsSqlServer
|
provider.template.client === TDbProviderClients.MsSqlServer
|
||||||
? ({
|
? ({
|
||||||
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
|
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
|
||||||
|
// when ca is provided use that
|
||||||
|
trustServerCertificate: !ca,
|
||||||
cryptoCredentialsDetails: ca ? { ca } : {}
|
cryptoCredentialsDetails: ca ? { ca } : {}
|
||||||
} as Record<string, unknown>)
|
} as Record<string, unknown>)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateTemplateDALFactory = ReturnType<typeof sshCertificateTemplateDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateTemplateDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCertificateTemplateOrm = ormify(db, TableName.SshCertificateTemplate);
|
||||||
|
|
||||||
|
const getById = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate)
|
||||||
|
.join(
|
||||||
|
TableName.SshCertificateAuthority,
|
||||||
|
`${TableName.SshCertificateAuthority}.id`,
|
||||||
|
`${TableName.SshCertificateTemplate}.sshCaId`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.SshCertificateAuthority}.projectId`)
|
||||||
|
.where(`${TableName.SshCertificateTemplate}.id`, "=", id)
|
||||||
|
.select(selectAllTableCols(TableName.SshCertificateTemplate))
|
||||||
|
.select(
|
||||||
|
db.ref("projectId").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return certTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Get SSH certificate template by ID" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SSH certificate template named [name] within project with id [projectId]
|
||||||
|
*/
|
||||||
|
const getByName = async (name: string, projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate)
|
||||||
|
.join(
|
||||||
|
TableName.SshCertificateAuthority,
|
||||||
|
`${TableName.SshCertificateAuthority}.id`,
|
||||||
|
`${TableName.SshCertificateTemplate}.sshCaId`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.SshCertificateAuthority}.projectId`)
|
||||||
|
.where(`${TableName.SshCertificateTemplate}.name`, "=", name)
|
||||||
|
.where(`${TableName.Project}.id`, "=", projectId)
|
||||||
|
.select(selectAllTableCols(TableName.SshCertificateTemplate))
|
||||||
|
.select(
|
||||||
|
db.ref("projectId").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return certTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Get SSH certificate template by name" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...sshCertificateTemplateOrm, getById, getByName };
|
||||||
|
};
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { SshCertificateTemplatesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshCertificateTemplate = SshCertificateTemplatesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
sshCaId: true,
|
||||||
|
status: true,
|
||||||
|
name: true,
|
||||||
|
ttl: true,
|
||||||
|
maxTTL: true,
|
||||||
|
allowedUsers: true,
|
||||||
|
allowedHosts: true,
|
||||||
|
allowCustomKeyIds: true,
|
||||||
|
allowUserCertificates: true,
|
||||||
|
allowHostCertificates: true
|
||||||
|
});
|
@@ -0,0 +1,249 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { ProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
||||||
|
import {
|
||||||
|
SshCertTemplateStatus,
|
||||||
|
TCreateSshCertTemplateDTO,
|
||||||
|
TDeleteSshCertTemplateDTO,
|
||||||
|
TGetSshCertTemplateDTO,
|
||||||
|
TUpdateSshCertTemplateDTO
|
||||||
|
} from "./ssh-certificate-template-types";
|
||||||
|
|
||||||
|
type TSshCertificateTemplateServiceFactoryDep = {
|
||||||
|
sshCertificateTemplateDAL: Pick<
|
||||||
|
TSshCertificateTemplateDALFactory,
|
||||||
|
"transaction" | "getByName" | "create" | "updateById" | "deleteById" | "getById"
|
||||||
|
>;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshCertificateTemplateServiceFactory = ReturnType<typeof sshCertificateTemplateServiceFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateTemplateServiceFactory = ({
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
permissionService
|
||||||
|
}: TSshCertificateTemplateServiceFactoryDep) => {
|
||||||
|
const createSshCertTemplate = async ({
|
||||||
|
sshCaId,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshCertTemplateDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(sshCaId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH CA with ID ${sshCaId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ms(ttl) > ms(maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TTL cannot be greater than max TTL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
|
||||||
|
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.projectId, tx);
|
||||||
|
if (existingTemplate) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH certificate template with name ${name} already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
status: SshCertTemplateStatus.ACTIVE
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificateTemplate: newCertificateTemplate, ca };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSshCertTemplate = async ({
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateSshCertTemplateDTO) => {
|
||||||
|
const certTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
|
||||||
|
if (name) {
|
||||||
|
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, certTemplate.projectId, tx);
|
||||||
|
if (existingTemplate && existingTemplate.id !== id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH certificate template with name ${name} already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TTL cannot be greater than max TTL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.updateById(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplate: updatedCertificateTemplate,
|
||||||
|
projectId: certTemplate.projectId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSshCertTemplate = async ({
|
||||||
|
id,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TDeleteSshCertTemplateDTO) => {
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certificateTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateTemplateDAL.deleteById(certificateTemplate.id);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshCertTemplateDTO) => {
|
||||||
|
const certTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
return certTemplate;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSshCertTemplate,
|
||||||
|
updateSshCertTemplate,
|
||||||
|
deleteSshCertTemplate,
|
||||||
|
getSshCertTemplate
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum SshCertTemplateStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateSshCertTemplateDTO = {
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
status?: SshCertTemplateStatus;
|
||||||
|
name?: string;
|
||||||
|
ttl?: string;
|
||||||
|
maxTTL?: string;
|
||||||
|
allowUserCertificates?: boolean;
|
||||||
|
allowHostCertificates?: boolean;
|
||||||
|
allowedUsers?: string[];
|
||||||
|
allowedHosts?: string[];
|
||||||
|
allowCustomKeyIds?: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
@@ -0,0 +1,14 @@
|
|||||||
|
// Validates usernames or wildcard (*)
|
||||||
|
export const isValidUserPattern = (value: string): boolean => {
|
||||||
|
// Matches valid Linux usernames or a wildcard (*)
|
||||||
|
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
||||||
|
return userRegex.test(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validates hostnames, wildcard domains, or IP addresses
|
||||||
|
export const isValidHostPattern = (value: string): boolean => {
|
||||||
|
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
||||||
|
const hostRegex =
|
||||||
|
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
||||||
|
return hostRegex.test(value);
|
||||||
|
};
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateBodyDALFactory = ReturnType<typeof sshCertificateBodyDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateBodyDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCertificateBodyOrm = ormify(db, TableName.SshCertificateBody);
|
||||||
|
return sshCertificateBodyOrm;
|
||||||
|
};
|
@@ -0,0 +1,38 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateDALFactory = ReturnType<typeof sshCertificateDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCertificateOrm = ormify(db, TableName.SshCertificate);
|
||||||
|
|
||||||
|
const countSshCertificatesInProject = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
interface CountResult {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = db
|
||||||
|
.replicaNode()(TableName.SshCertificate)
|
||||||
|
.join(
|
||||||
|
TableName.SshCertificateAuthority,
|
||||||
|
`${TableName.SshCertificate}.sshCaId`,
|
||||||
|
`${TableName.SshCertificateAuthority}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.SshCertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.where(`${TableName.Project}.id`, projectId);
|
||||||
|
|
||||||
|
const count = await query.count("*").first();
|
||||||
|
|
||||||
|
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Count all SSH certificates in project" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...sshCertificateOrm,
|
||||||
|
countSshCertificatesInProject
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,14 @@
|
|||||||
|
import { SshCertificatesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshCertificate = SshCertificatesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
sshCaId: true,
|
||||||
|
sshCertificateTemplateId: true,
|
||||||
|
serialNumber: true,
|
||||||
|
certType: true,
|
||||||
|
publicKey: true,
|
||||||
|
principals: true,
|
||||||
|
keyId: true,
|
||||||
|
notBefore: true,
|
||||||
|
notAfter: true
|
||||||
|
});
|
10
backend/src/ee/services/ssh/ssh-certificate-authority-dal.ts
Normal file
10
backend/src/ee/services/ssh/ssh-certificate-authority-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 TSshCertificateAuthorityDALFactory = ReturnType<typeof sshCertificateAuthorityDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthorityDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCaOrm = ormify(db, TableName.SshCertificateAuthority);
|
||||||
|
return sshCaOrm;
|
||||||
|
};
|
376
backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts
Normal file
376
backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import { execFile } from "child_process";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import ms from "ms";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isValidHostPattern,
|
||||||
|
isValidUserPattern
|
||||||
|
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
|
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
export const createSshCertSerialNumber = () => {
|
||||||
|
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
||||||
|
randomBytes[0] &= 0x7f; // Ensure the most significant bit is 0 (to stay within unsigned range)
|
||||||
|
return BigInt(`0x${randomBytes.toString("hex")}`).toString(10); // Convert to decimal
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
||||||
|
* We use this function because the key format generated by `ssh-keygen` is unique.
|
||||||
|
*/
|
||||||
|
export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
||||||
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
|
const publicKeyFile = `${privateKeyFile}.pub`;
|
||||||
|
|
||||||
|
let keyType: string;
|
||||||
|
let keyBits: string;
|
||||||
|
|
||||||
|
switch (keyAlgorithm) {
|
||||||
|
case CertKeyAlgorithm.RSA_2048:
|
||||||
|
keyType = "rsa";
|
||||||
|
keyBits = "2048";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.RSA_4096:
|
||||||
|
keyType = "rsa";
|
||||||
|
keyBits = "4096";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.ECDSA_P256:
|
||||||
|
keyType = "ecdsa";
|
||||||
|
keyBits = "256";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.ECDSA_P384:
|
||||||
|
keyType = "ecdsa";
|
||||||
|
keyBits = "384";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate the SSH key pair
|
||||||
|
// The "-N ''" sets an empty passphrase
|
||||||
|
// The keys are created in the temporary directory
|
||||||
|
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""]);
|
||||||
|
|
||||||
|
// Read the generated keys
|
||||||
|
const publicKey = await fs.readFile(publicKeyFile, "utf8");
|
||||||
|
const privateKey = await fs.readFile(privateKeyFile, "utf8");
|
||||||
|
|
||||||
|
return { publicKey, privateKey };
|
||||||
|
} finally {
|
||||||
|
// Cleanup the temporary directory and all its contents
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the SSH public key for the given SSH private key.
|
||||||
|
*/
|
||||||
|
export const getSshPublicKey = async (privateKey: string) => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
||||||
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
|
try {
|
||||||
|
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
|
||||||
|
|
||||||
|
// Run ssh-keygen to extract the public key
|
||||||
|
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { encoding: "utf8" });
|
||||||
|
return stdout.trim();
|
||||||
|
} finally {
|
||||||
|
// Ensure that files and the temporary directory are cleaned up
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate type based on the SSH certificate template configuration.
|
||||||
|
*/
|
||||||
|
export const validateSshCertificateType = (template: TSshCertificateTemplates, certType: SshCertType) => {
|
||||||
|
if (!template.allowUserCertificates && certType === SshCertType.USER) {
|
||||||
|
throw new BadRequestError({ message: "Failed to validate user certificate type due to template restriction" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!template.allowHostCertificates && certType === SshCertType.HOST) {
|
||||||
|
throw new BadRequestError({ message: "Failed to validate host certificate type due to template restriction" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate principals based on the SSH certificate template configuration.
|
||||||
|
*/
|
||||||
|
export const validateSshCertificatePrincipals = (
|
||||||
|
certType: SshCertType,
|
||||||
|
template: TSshCertificateTemplates,
|
||||||
|
principals: string[]
|
||||||
|
) => {
|
||||||
|
/**
|
||||||
|
* Validate and sanitize a principal string
|
||||||
|
*/
|
||||||
|
const validatePrincipal = (principal: string) => {
|
||||||
|
const sanitized = principal.trim();
|
||||||
|
|
||||||
|
// basic checks for empty or control characters
|
||||||
|
if (sanitized.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Principal cannot be an empty string."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\r|\n|\t|\0/.test(sanitized)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' contains invalid whitespace or control characters.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow whitespace anywhere
|
||||||
|
if (/\s/.test(sanitized)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' cannot contain whitespace.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// restrict allowed characters to letters, digits, dot, underscore, and hyphen
|
||||||
|
if (!/^[A-Za-z0-9._-]+$/.test(sanitized)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow leading hyphen to avoid potential argument-like inputs
|
||||||
|
if (sanitized.startsWith("-")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' cannot start with a hyphen.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// length restriction (adjust as needed)
|
||||||
|
if (sanitized.length > 64) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' is too long.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanitize and validate all principals using the helper
|
||||||
|
const sanitizedPrincipals = principals.map(validatePrincipal);
|
||||||
|
|
||||||
|
switch (certType) {
|
||||||
|
case SshCertType.USER: {
|
||||||
|
if (template.allowedUsers.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "No allowed users are configured in the SSH certificate template."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowsAllUsers = template.allowedUsers.includes("*") ?? false;
|
||||||
|
|
||||||
|
sanitizedPrincipals.forEach((principal) => {
|
||||||
|
if (principal === "*") {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '*' is not allowed for user certificates.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (allowsAllUsers && !isValidUserPattern(principal)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' does not match a valid user pattern.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!allowsAllUsers && !template.allowedUsers.includes(principal)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' is not in the list of allowed users.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SshCertType.HOST: {
|
||||||
|
if (template.allowedHosts.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "No allowed hosts are configured in the SSH certificate template."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowsAllHosts = template.allowedHosts.includes("*") ?? false;
|
||||||
|
|
||||||
|
sanitizedPrincipals.forEach((principal) => {
|
||||||
|
if (principal.includes("*")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' with wildcards is not allowed for host certificates.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (allowsAllHosts && !isValidHostPattern(principal)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' does not match a valid host pattern.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allowsAllHosts &&
|
||||||
|
!template.allowedHosts.some((allowedHost) => {
|
||||||
|
if (allowedHost.startsWith("*.")) {
|
||||||
|
const baseDomain = allowedHost.slice(2); // Remove the leading "*."
|
||||||
|
return principal.endsWith(`.${baseDomain}`);
|
||||||
|
}
|
||||||
|
return principal === allowedHost;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' is not in the list of allowed hosts or domains.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH certificate principals due to unrecognized requested certificate type"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate TTL based on the SSH certificate template configuration.
|
||||||
|
*/
|
||||||
|
export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl?: string) => {
|
||||||
|
if (!ttl) {
|
||||||
|
// use default template ttl
|
||||||
|
return Math.ceil(ms(template.ttl) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms(ttl) > ms(template.maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed TTL validation due to TTL being greater than configured max TTL on template"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(ms(ttl) / 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate key ID to ensure
|
||||||
|
* that it only contains alphanumeric characters with no spaces.
|
||||||
|
*/
|
||||||
|
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||||
|
const regex = /^[A-Za-z0-9-]+$/;
|
||||||
|
if (!regex.test(keyId)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyId.length > 50) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "keyId can only be up to 50 characters long."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the format of the SSH public key
|
||||||
|
*/
|
||||||
|
const validateSshPublicKey = async (publicKey: string) => {
|
||||||
|
const validPrefixes = ["ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"];
|
||||||
|
const startsWithValidPrefix = validPrefixes.some((prefix) => publicKey.startsWith(`${prefix} `));
|
||||||
|
if (!startsWithValidPrefix) {
|
||||||
|
throw new BadRequestError({ message: "Failed to validate SSH public key format: unsupported key type." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the key to a temp file and run `ssh-keygen -l -f`
|
||||||
|
// check to see if OpenSSH can read/interpret the public key
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-pubkey-"));
|
||||||
|
const pubKeyFile = path.join(tempDir, "key.pub");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 });
|
||||||
|
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile]);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH public key format: could not be parsed."
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an SSH certificate for a user or host.
|
||||||
|
*/
|
||||||
|
export const createSshCert = async ({
|
||||||
|
template,
|
||||||
|
caPrivateKey,
|
||||||
|
clientPublicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl,
|
||||||
|
certType
|
||||||
|
}: TCreateSshCertDTO) => {
|
||||||
|
// validate if the requested [certType] is allowed under the template configuration
|
||||||
|
validateSshCertificateType(template, certType);
|
||||||
|
|
||||||
|
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
||||||
|
validateSshCertificatePrincipals(certType, template, principals);
|
||||||
|
|
||||||
|
// validate if the requested TTL is valid under the template configuration
|
||||||
|
const ttl = validateSshCertificateTtl(template, requestedTtl);
|
||||||
|
|
||||||
|
validateSshCertificateKeyId(keyId);
|
||||||
|
await validateSshPublicKey(clientPublicKey);
|
||||||
|
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-cert-"));
|
||||||
|
|
||||||
|
const publicKeyFile = path.join(tempDir, "user_key.pub");
|
||||||
|
const privateKeyFile = path.join(tempDir, "ca_key");
|
||||||
|
const signedPublicKeyFile = path.join(tempDir, "user_key-cert.pub");
|
||||||
|
|
||||||
|
const serialNumber = createSshCertSerialNumber();
|
||||||
|
|
||||||
|
// Build `ssh-keygen` arguments for signing
|
||||||
|
// Using an array avoids shell injection issues
|
||||||
|
const sshKeygenArgs = [
|
||||||
|
certType === "host" ? "-h" : null, // host certificate if needed
|
||||||
|
"-s",
|
||||||
|
privateKeyFile, // path to SSH CA private key
|
||||||
|
"-I",
|
||||||
|
keyId, // identity (key ID)
|
||||||
|
"-n",
|
||||||
|
principals.join(","), // principals
|
||||||
|
"-V",
|
||||||
|
`+${ttl}s`, // validity (TTL in seconds)
|
||||||
|
"-z",
|
||||||
|
serialNumber, // serial number
|
||||||
|
publicKeyFile // public key file to sign
|
||||||
|
].filter(Boolean) as string[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write public and private keys to the temp directory
|
||||||
|
await fs.writeFile(publicKeyFile, clientPublicKey, { mode: 0o600 });
|
||||||
|
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
|
||||||
|
|
||||||
|
// Execute the signing process
|
||||||
|
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8" });
|
||||||
|
|
||||||
|
// Read the signed public key from the generated cert file
|
||||||
|
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
|
||||||
|
|
||||||
|
return { serialNumber, signedPublicKey, ttl };
|
||||||
|
} finally {
|
||||||
|
// Cleanup the temporary directory and all its contents
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { SshCertificateAuthoritiesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
projectId: true,
|
||||||
|
friendlyName: true,
|
||||||
|
status: true,
|
||||||
|
keyAlgorithm: true
|
||||||
|
});
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateAuthoritySecretDALFactory = ReturnType<typeof sshCertificateAuthoritySecretDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthoritySecretDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCaSecretOrm = ormify(db, TableName.SshCertificateAuthoritySecret);
|
||||||
|
return sshCaSecretOrm;
|
||||||
|
};
|
523
backend/src/ee/services/ssh/ssh-certificate-authority-service.ts
Normal file
523
backend/src/ee/services/ssh/ssh-certificate-authority-service.ts
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
|
||||||
|
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
|
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
||||||
|
import { createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
||||||
|
import {
|
||||||
|
SshCaStatus,
|
||||||
|
TCreateSshCaDTO,
|
||||||
|
TDeleteSshCaDTO,
|
||||||
|
TGetSshCaCertificateTemplatesDTO,
|
||||||
|
TGetSshCaDTO,
|
||||||
|
TGetSshCaPublicKeyDTO,
|
||||||
|
TIssueSshCredsDTO,
|
||||||
|
TSignSshKeyDTO,
|
||||||
|
TUpdateSshCaDTO
|
||||||
|
} from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
|
type TSshCertificateAuthorityServiceFactoryDep = {
|
||||||
|
sshCertificateAuthorityDAL: Pick<
|
||||||
|
TSshCertificateAuthorityDALFactory,
|
||||||
|
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
|
||||||
|
>;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||||
|
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find" | "getById">;
|
||||||
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
||||||
|
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
||||||
|
kmsService: Pick<
|
||||||
|
TKmsServiceFactory,
|
||||||
|
"generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId" | "createCipherPairWithDataKey"
|
||||||
|
>;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshCertificateAuthorityServiceFactory = ReturnType<typeof sshCertificateAuthorityServiceFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthorityServiceFactory = ({
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateBodyDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
}: TSshCertificateAuthorityServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Generates a new SSH CA
|
||||||
|
*/
|
||||||
|
const createSshCa = async ({
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
keyAlgorithm,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshCaDTO) => {
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const newCa = await sshCertificateAuthorityDAL.transaction(async (tx) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
status: SshCaStatus.ACTIVE,
|
||||||
|
keyAlgorithm
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
await sshCertificateAuthoritySecretDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: ca.id,
|
||||||
|
encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...ca, publicKey };
|
||||||
|
});
|
||||||
|
|
||||||
|
return newCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getSshCaById = async ({ caId, actor, actorId, actorAuthMethod, actorOrgId }: TGetSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: ca.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return { ...ca, publicKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return public key of SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getSshCaPublicKey = async ({ caId }: TGetSshCaPublicKeyDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: ca.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update SSH CA with id [caId]
|
||||||
|
* Note: Used to enable/disable CA
|
||||||
|
*/
|
||||||
|
const updateSshCaById = async ({
|
||||||
|
caId,
|
||||||
|
friendlyName,
|
||||||
|
status,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCa = await sshCertificateAuthorityDAL.updateById(caId, { friendlyName, status });
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: ca.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return { ...updatedCa, publicKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const deleteSshCaById = async ({ caId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedCa = await sshCertificateAuthorityDAL.deleteById(caId);
|
||||||
|
|
||||||
|
return deletedCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH certificate and corresponding new SSH public-private key pair where
|
||||||
|
* SSH public key is signed using CA behind SSH certificate with name [templateName].
|
||||||
|
*/
|
||||||
|
const issueSshCreds = async ({
|
||||||
|
certificateTemplateId,
|
||||||
|
keyAlgorithm,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
ttl: requestedTtl,
|
||||||
|
keyId: requestedKeyId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueSshCredsDTO) => {
|
||||||
|
const sshCertificateTemplate = await sshCertificateTemplateDAL.getById(certificateTemplateId);
|
||||||
|
if (!sshCertificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "No SSH certificate template found with specified name"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
sshCertificateTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SshCertificates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH CA is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH certificate template is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
|
? requestedKeyId ?? `${actor}-${actorId}`
|
||||||
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: sshCertificateTemplate.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// create user key pair
|
||||||
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||||
|
template: sshCertificateTemplate,
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
clientPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl,
|
||||||
|
certType
|
||||||
|
});
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: sshCertificateTemplate.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedCertificate = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(signedPublicKey, "utf8")
|
||||||
|
}).cipherTextBlob;
|
||||||
|
|
||||||
|
await sshCertificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await sshCertificateDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: sshCertificateTemplate.sshCaId,
|
||||||
|
sshCertificateTemplateId: sshCertificateTemplate.id,
|
||||||
|
serialNumber,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
keyId,
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(Date.now() + ttl * 1000)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
sshCertId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
certificateTemplate: sshCertificateTemplate,
|
||||||
|
ttl,
|
||||||
|
keyId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH certificate by signing SSH public key [publicKey]
|
||||||
|
* using CA behind SSH certificate template with name [templateName]
|
||||||
|
*/
|
||||||
|
const signSshKey = async ({
|
||||||
|
certificateTemplateId,
|
||||||
|
publicKey,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
ttl: requestedTtl,
|
||||||
|
keyId: requestedKeyId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TSignSshKeyDTO) => {
|
||||||
|
const sshCertificateTemplate = await sshCertificateTemplateDAL.getById(certificateTemplateId);
|
||||||
|
if (!sshCertificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "No SSH certificate template found with specified name"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
sshCertificateTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SshCertificates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH CA is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH certificate template is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
|
? requestedKeyId ?? `${actor}-${actorId}`
|
||||||
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: sshCertificateTemplate.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||||
|
template: sshCertificateTemplate,
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
clientPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl,
|
||||||
|
certType
|
||||||
|
});
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: sshCertificateTemplate.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedCertificate = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(signedPublicKey, "utf8")
|
||||||
|
}).cipherTextBlob;
|
||||||
|
|
||||||
|
await sshCertificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await sshCertificateDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: sshCertificateTemplate.sshCaId,
|
||||||
|
sshCertificateTemplateId: sshCertificateTemplate.id,
|
||||||
|
serialNumber,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
keyId,
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(Date.now() + ttl * 1000)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
sshCertId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { serialNumber, signedPublicKey, certificateTemplate: sshCertificateTemplate, ttl, keyId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshCaCertificateTemplates = async ({
|
||||||
|
caId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetSshCaCertificateTemplatesDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
const certificateTemplates = await sshCertificateTemplateDAL.find({ sshCaId: caId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplates,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueSshCreds,
|
||||||
|
signSshKey,
|
||||||
|
createSshCa,
|
||||||
|
getSshCaById,
|
||||||
|
getSshCaPublicKey,
|
||||||
|
updateSshCaById,
|
||||||
|
deleteSshCaById,
|
||||||
|
getSshCaCertificateTemplates
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
export enum SshCaStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SshCertType {
|
||||||
|
USER = "user",
|
||||||
|
HOST = "host"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateSshCaDTO = {
|
||||||
|
friendlyName: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshCaPublicKeyDTO = {
|
||||||
|
caId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
status?: SshCaStatus;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueSshCredsDTO = {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl?: string;
|
||||||
|
keyId?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TSignSshKeyDTO = {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
publicKey: string;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl?: string;
|
||||||
|
keyId?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshCaCertificateTemplatesDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TCreateSshCertDTO = {
|
||||||
|
template: TSshCertificateTemplates;
|
||||||
|
caPrivateKey: string;
|
||||||
|
clientPublicKey: string;
|
||||||
|
keyId: string;
|
||||||
|
principals: string[];
|
||||||
|
requestedTtl?: string;
|
||||||
|
certType: SshCertType;
|
||||||
|
};
|
@@ -1,3 +1,6 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
|
||||||
export const GROUPS = {
|
export const GROUPS = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
name: "The name of the group to create.",
|
name: "The name of the group to create.",
|
||||||
@@ -492,6 +495,17 @@ export const PROJECTS = {
|
|||||||
LIST_INTEGRATION_AUTHORIZATION: {
|
LIST_INTEGRATION_AUTHORIZATION: {
|
||||||
workspaceId: "The ID of the project to list integration auths for."
|
workspaceId: "The ID of the project to list integration auths for."
|
||||||
},
|
},
|
||||||
|
LIST_SSH_CAS: {
|
||||||
|
projectId: "The ID of the project to list SSH CAs for."
|
||||||
|
},
|
||||||
|
LIST_SSH_CERTIFICATES: {
|
||||||
|
projectId: "The ID of the project to list SSH certificates for.",
|
||||||
|
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
|
||||||
|
limit: "The number of SSH certificates to return."
|
||||||
|
},
|
||||||
|
LIST_SSH_CERTIFICATE_TEMPLATES: {
|
||||||
|
projectId: "The ID of the project to list SSH certificate templates for."
|
||||||
|
},
|
||||||
LIST_CAS: {
|
LIST_CAS: {
|
||||||
slug: "The slug of the project to list CAs for.",
|
slug: "The slug of the project to list CAs for.",
|
||||||
status: "The status of the CA to filter by.",
|
status: "The status of the CA to filter by.",
|
||||||
@@ -727,6 +741,12 @@ export const RAW_SECRETS = {
|
|||||||
workspaceId: "The ID of the project where the secret is located.",
|
workspaceId: "The ID of the project where the secret is located.",
|
||||||
environment: "The slug of the environment where the the secret is located.",
|
environment: "The slug of the environment where the the secret is located.",
|
||||||
secretPath: "The folder path where the secret is located."
|
secretPath: "The folder path where the secret is located."
|
||||||
|
},
|
||||||
|
GET_ACCESS_LIST: {
|
||||||
|
secretName: "The name of the secret to get the access list for.",
|
||||||
|
workspaceId: "The ID of the project where the secret is located.",
|
||||||
|
environment: "The slug of the environment where the the secret is located.",
|
||||||
|
secretPath: "The folder path where the secret is located."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -1126,6 +1146,7 @@ export const INTEGRATION = {
|
|||||||
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
||||||
secretGCPLabel: "The label for GCP secrets.",
|
secretGCPLabel: "The label for GCP secrets.",
|
||||||
secretAWSTag: "The tags for AWS secrets.",
|
secretAWSTag: "The tags for AWS secrets.",
|
||||||
|
azureLabel: "Define which label to assign to secrets created in Azure App Configuration.",
|
||||||
githubVisibility:
|
githubVisibility:
|
||||||
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
|
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
|
||||||
githubVisibilityRepoIds:
|
githubVisibilityRepoIds:
|
||||||
@@ -1135,7 +1156,8 @@ export const INTEGRATION = {
|
|||||||
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
|
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
|
||||||
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
|
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
|
||||||
shouldEnableDelete: "The flag to enable deletion of secrets.",
|
shouldEnableDelete: "The flag to enable deletion of secrets.",
|
||||||
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy."
|
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy.",
|
||||||
|
metadataSyncMode: "The mode for syncing metadata to external system"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
@@ -1186,6 +1208,84 @@ export const AUDIT_LOG_STREAMS = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SSH_CERTIFICATE_AUTHORITIES = {
|
||||||
|
CREATE: {
|
||||||
|
projectId: "The ID of the project to create the SSH CA in.",
|
||||||
|
friendlyName: "A friendly name for the SSH CA.",
|
||||||
|
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA."
|
||||||
|
},
|
||||||
|
GET: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get."
|
||||||
|
},
|
||||||
|
GET_PUBLIC_KEY: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get the public key for."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to update.",
|
||||||
|
friendlyName: "A friendly name for the SSH CA to update to.",
|
||||||
|
status: "The status of the SSH CA to update to. This can be one of active or disabled."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to delete."
|
||||||
|
},
|
||||||
|
GET_CERTIFICATE_TEMPLATES: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get the certificate templates for."
|
||||||
|
},
|
||||||
|
SIGN_SSH_KEY: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to sign the SSH public key with.",
|
||||||
|
publicKey: "The SSH public key to sign.",
|
||||||
|
certType: "The type of certificate to issue. This can be one of user or host.",
|
||||||
|
principals: "The list of principals (usernames, hostnames) to include in the certificate.",
|
||||||
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, ... If not specified, the default TTL for the template will be used.",
|
||||||
|
keyId: "The key ID to include in the certificate. If not specified, a default key ID will be generated.",
|
||||||
|
serialNumber: "The serial number of the issued SSH certificate.",
|
||||||
|
signedKey: "The SSH certificate or signed SSH public key."
|
||||||
|
},
|
||||||
|
ISSUE_SSH_CREDENTIALS: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to issue the SSH credentials with.",
|
||||||
|
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA.",
|
||||||
|
certType: "The type of certificate to issue. This can be one of user or host.",
|
||||||
|
principals: "The list of principals (usernames, hostnames) to include in the certificate.",
|
||||||
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, ... If not specified, the default TTL for the template will be used.",
|
||||||
|
keyId: "The key ID to include in the certificate. If not specified, a default key ID will be generated.",
|
||||||
|
serialNumber: "The serial number of the issued SSH certificate.",
|
||||||
|
signedKey: "The SSH certificate or signed SSH public key.",
|
||||||
|
privateKey: "The private key corresponding to the issued SSH certificate.",
|
||||||
|
publicKey: "The public key of the issued SSH certificate."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SSH_CERTIFICATE_TEMPLATES = {
|
||||||
|
GET: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to get."
|
||||||
|
},
|
||||||
|
CREATE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to associate the certificate template with.",
|
||||||
|
name: "The name of the certificate template.",
|
||||||
|
ttl: "The default time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
maxTTL: "The maximum time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
allowedUsers: "The list of allowed users for certificates issued under this template.",
|
||||||
|
allowedHosts: "The list of allowed hosts for certificates issued under this template.",
|
||||||
|
allowUserCertificates: "Whether or not to allow user certificates to be issued under this template.",
|
||||||
|
allowHostCertificates: "Whether or not to allow host certificates to be issued under this template.",
|
||||||
|
allowCustomKeyIds: "Whether or not to allow custom key IDs for certificates issued under this template."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to update.",
|
||||||
|
name: "The name of the certificate template.",
|
||||||
|
ttl: "The default time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
maxTTL: "The maximum time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
allowedUsers: "The list of allowed users for certificates issued under this template.",
|
||||||
|
allowedHosts: "The list of allowed hosts for certificates issued under this template.",
|
||||||
|
allowUserCertificates: "Whether or not to allow user certificates to be issued under this template.",
|
||||||
|
allowHostCertificates: "Whether or not to allow host certificates to be issued under this template.",
|
||||||
|
allowCustomKeyIds: "Whether or not to allow custom key IDs for certificates issued under this template."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to delete."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES = {
|
export const CERTIFICATE_AUTHORITIES = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectSlug: "Slug of the project to create the CA in.",
|
projectSlug: "Slug of the project to create the CA in.",
|
||||||
@@ -1515,3 +1615,34 @@ export const ProjectTemplates = {
|
|||||||
templateId: "The ID of the project template to be deleted."
|
templateId: "The ID of the project template to be deleted."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AppConnections = {
|
||||||
|
GET_BY_ID: (app: AppConnection) => ({
|
||||||
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||||
|
}),
|
||||||
|
GET_BY_NAME: (app: AppConnection) => ({
|
||||||
|
connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||||
|
}),
|
||||||
|
CREATE: (app: AppConnection) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
return {
|
||||||
|
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||||
|
description: `An optional description for the ${appName} Connection.`,
|
||||||
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
|
method: `The method used to authenticate with ${appName}.`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
UPDATE: (app: AppConnection) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
return {
|
||||||
|
connectionId: `The ID of the ${appName} Connection to be updated.`,
|
||||||
|
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||||
|
description: `The updated description of the ${appName} Connection.`,
|
||||||
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
|
method: `The method used to authenticate with ${appName}.`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
DELETE: (app: AppConnection) => ({
|
||||||
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} connection to be deleted.`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
@@ -157,6 +157,8 @@ const envSchema = z
|
|||||||
INFISICAL_CLOUD: zodStrBool.default("false"),
|
INFISICAL_CLOUD: zodStrBool.default("false"),
|
||||||
MAINTENANCE_MODE: zodStrBool.default("false"),
|
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||||
|
CAPTCHA_SITE_KEY: zpStr(z.string().optional()),
|
||||||
|
INTERCOM_ID: zpStr(z.string().optional()),
|
||||||
|
|
||||||
// TELEMETRY
|
// TELEMETRY
|
||||||
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
|
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
|
||||||
@@ -180,7 +182,24 @@ const envSchema = z
|
|||||||
HSM_SLOT: z.coerce.number().optional().default(0),
|
HSM_SLOT: z.coerce.number().optional().default(0),
|
||||||
|
|
||||||
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"),
|
||||||
|
|
||||||
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// aws
|
||||||
|
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// github oauth
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// github app
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional())
|
||||||
})
|
})
|
||||||
// To ensure that basic encryption is always possible.
|
// To ensure that basic encryption is always possible.
|
||||||
.refine(
|
.refine(
|
||||||
|
@@ -14,3 +14,5 @@ export const prefixWithSlash = (str: string) => {
|
|||||||
if (str.startsWith("/")) return str;
|
if (str.startsWith("/")) return str;
|
||||||
return `/${str}`;
|
return `/${str}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
|
||||||
|
@@ -20,11 +20,12 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
|||||||
|
|
||||||
export type TFindFilter<R extends object = object> = Partial<R> & {
|
export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||||
|
$notNull?: Array<keyof R>;
|
||||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||||
$complex?: TKnexDynamicOperator<R>;
|
$complex?: TKnexDynamicOperator<R>;
|
||||||
};
|
};
|
||||||
export const buildFindFilter =
|
export const buildFindFilter =
|
||||||
<R extends object = object>({ $in, $search, $complex, ...filter }: TFindFilter<R>) =>
|
<R extends object = object>({ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>) =>
|
||||||
(bd: Knex.QueryBuilder<R, R>) => {
|
(bd: Knex.QueryBuilder<R, R>) => {
|
||||||
void bd.where(filter);
|
void bd.where(filter);
|
||||||
if ($in) {
|
if ($in) {
|
||||||
@@ -34,6 +35,13 @@ export const buildFindFilter =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($notNull?.length) {
|
||||||
|
$notNull.forEach((key) => {
|
||||||
|
void bd.whereNotNull(key as never);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
Object.entries($search).forEach(([key, val]) => {
|
Object.entries($search).forEach(([key, val]) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
|
@@ -43,6 +43,8 @@ export type RequiredKeys<T> = {
|
|||||||
|
|
||||||
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
||||||
|
|
||||||
|
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
||||||
|
|
||||||
export enum EnforcementLevel {
|
export enum EnforcementLevel {
|
||||||
Hard = "hard",
|
Hard = "hard",
|
||||||
Soft = "soft"
|
Soft = "soft"
|
||||||
|
@@ -317,6 +317,13 @@ export const queueServiceFactory = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRepeatableJobs = (name: QueueName, startOffset?: number, endOffset?: number) => {
|
||||||
|
const q = queueContainer[name];
|
||||||
|
if (!q) throw new Error(`Queue '${name}' not initialized`);
|
||||||
|
|
||||||
|
return q.getRepeatableJobs(startOffset, endOffset);
|
||||||
|
};
|
||||||
|
|
||||||
const stopRepeatableJobByJobId = async <T extends QueueName>(name: T, jobId: string) => {
|
const stopRepeatableJobByJobId = async <T extends QueueName>(name: T, jobId: string) => {
|
||||||
const q = queueContainer[name];
|
const q = queueContainer[name];
|
||||||
const job = await q.getJob(jobId);
|
const job = await q.getJob(jobId);
|
||||||
@@ -326,6 +333,11 @@ export const queueServiceFactory = (
|
|||||||
return q.removeRepeatableByKey(job.repeatJobKey);
|
return q.removeRepeatableByKey(job.repeatJobKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopRepeatableJobByKey = async <T extends QueueName>(name: T, repeatJobKey: string) => {
|
||||||
|
const q = queueContainer[name];
|
||||||
|
return q.removeRepeatableByKey(repeatJobKey);
|
||||||
|
};
|
||||||
|
|
||||||
const stopJobById = async <T extends QueueName>(name: T, jobId: string) => {
|
const stopJobById = async <T extends QueueName>(name: T, jobId: string) => {
|
||||||
const q = queueContainer[name];
|
const q = queueContainer[name];
|
||||||
const job = await q.getJob(jobId);
|
const job = await q.getJob(jobId);
|
||||||
@@ -349,8 +361,10 @@ export const queueServiceFactory = (
|
|||||||
shutdown,
|
shutdown,
|
||||||
stopRepeatableJob,
|
stopRepeatableJob,
|
||||||
stopRepeatableJobByJobId,
|
stopRepeatableJobByJobId,
|
||||||
|
stopRepeatableJobByKey,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
stopJobById,
|
stopJobById,
|
||||||
|
getRepeatableJobs,
|
||||||
startPg,
|
startPg,
|
||||||
queuePg
|
queuePg
|
||||||
};
|
};
|
||||||
|
@@ -27,10 +27,10 @@ import { globalRateLimiterCfg } from "./config/rateLimiter";
|
|||||||
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
|
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
|
||||||
import { apiMetrics } from "./plugins/api-metrics";
|
import { apiMetrics } from "./plugins/api-metrics";
|
||||||
import { fastifyErrHandler } from "./plugins/error-handler";
|
import { fastifyErrHandler } from "./plugins/error-handler";
|
||||||
import { registerExternalNextjs } from "./plugins/external-nextjs";
|
|
||||||
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
||||||
import { fastifyIp } from "./plugins/ip";
|
import { fastifyIp } from "./plugins/ip";
|
||||||
import { maintenanceMode } from "./plugins/maintenanceMode";
|
import { maintenanceMode } from "./plugins/maintenanceMode";
|
||||||
|
import { registerServeUI } from "./plugins/serve-ui";
|
||||||
import { fastifySwagger } from "./plugins/swagger";
|
import { fastifySwagger } from "./plugins/swagger";
|
||||||
import { registerRoutes } from "./routes";
|
import { registerRoutes } from "./routes";
|
||||||
|
|
||||||
@@ -120,13 +120,10 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
|
|||||||
|
|
||||||
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
|
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
|
||||||
|
|
||||||
if (appCfg.isProductionMode) {
|
await server.register(registerServeUI, {
|
||||||
await server.register(registerExternalNextjs, {
|
standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED,
|
||||||
standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED,
|
dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../")
|
||||||
dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../"),
|
});
|
||||||
port: appCfg.PORT
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.ready();
|
await server.ready();
|
||||||
server.swagger();
|
server.swagger();
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import { ForbiddenError, PureAbility } from "@casl/ability";
|
import { ForbiddenError, PureAbility } from "@casl/ability";
|
||||||
|
import opentelemetry from "@opentelemetry/api";
|
||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import {
|
import {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
@@ -35,8 +37,30 @@ enum HttpStatusCodes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const apiMeter = opentelemetry.metrics.getMeter("API");
|
||||||
|
const errorHistogram = apiMeter.createHistogram("API_errors", {
|
||||||
|
description: "API errors by type, status code, and name",
|
||||||
|
unit: "1"
|
||||||
|
});
|
||||||
|
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
const { method } = req;
|
||||||
|
const route = req.routerPath;
|
||||||
|
const errorType =
|
||||||
|
error instanceof jwt.JsonWebTokenError ? "TokenError" : error.constructor.name || "UnknownError";
|
||||||
|
|
||||||
|
errorHistogram.record(1, {
|
||||||
|
route,
|
||||||
|
method,
|
||||||
|
type: errorType,
|
||||||
|
name: error.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
void res
|
void res
|
||||||
.status(HttpStatusCodes.BadRequest)
|
.status(HttpStatusCodes.BadRequest)
|
||||||
@@ -52,13 +76,20 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
message: error.message,
|
message: error.message,
|
||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
} else if (error instanceof DatabaseError) {
|
||||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
statusCode: HttpStatusCodes.InternalServerError,
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
message: "Something went wrong",
|
message: "Something went wrong",
|
||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
|
} else if (error instanceof InternalServerError) {
|
||||||
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
|
reqId: req.id,
|
||||||
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
|
message: error.message ?? "Something went wrong",
|
||||||
|
error: error.name
|
||||||
|
});
|
||||||
} else if (error instanceof GatewayTimeoutError) {
|
} else if (error instanceof GatewayTimeoutError) {
|
||||||
void res.status(HttpStatusCodes.GatewayTimeout).send({
|
void res.status(HttpStatusCodes.GatewayTimeout).send({
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
|
@@ -1,76 +0,0 @@
|
|||||||
// this plugins allows to run infisical in standalone mode
|
|
||||||
// standalone mode = infisical backend and nextjs frontend in one server
|
|
||||||
// this way users don't need to deploy two things
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import { IS_PACKAGED } from "@app/lib/config/env";
|
|
||||||
|
|
||||||
// to enabled this u need to set standalone mode to true
|
|
||||||
export const registerExternalNextjs = async (
|
|
||||||
server: FastifyZodProvider,
|
|
||||||
{
|
|
||||||
standaloneMode,
|
|
||||||
dir,
|
|
||||||
port
|
|
||||||
}: {
|
|
||||||
standaloneMode?: boolean;
|
|
||||||
dir: string;
|
|
||||||
port: number;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
if (standaloneMode) {
|
|
||||||
const frontendName = IS_PACKAGED ? "frontend" : "frontend-build";
|
|
||||||
const nextJsBuildPath = path.join(dir, frontendName);
|
|
||||||
|
|
||||||
const { default: conf } = (await import(
|
|
||||||
path.join(dir, `${frontendName}/.next/required-server-files.json`),
|
|
||||||
// @ts-expect-error type
|
|
||||||
{
|
|
||||||
assert: { type: "json" }
|
|
||||||
}
|
|
||||||
)) as { default: { config: string } };
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let NextServer: any;
|
|
||||||
|
|
||||||
if (!IS_PACKAGED) {
|
|
||||||
/* eslint-disable */
|
|
||||||
const { default: nextServer } = (
|
|
||||||
await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`))
|
|
||||||
).default;
|
|
||||||
|
|
||||||
NextServer = nextServer;
|
|
||||||
} else {
|
|
||||||
/* eslint-disable */
|
|
||||||
const nextServer = await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`));
|
|
||||||
|
|
||||||
NextServer = nextServer.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextApp = new NextServer({
|
|
||||||
dev: false,
|
|
||||||
dir: nextJsBuildPath,
|
|
||||||
port,
|
|
||||||
conf: conf.config,
|
|
||||||
hostname: "local",
|
|
||||||
customServer: false
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: ["GET", "PUT", "PATCH", "POST", "DELETE"],
|
|
||||||
url: "/*",
|
|
||||||
schema: {
|
|
||||||
hide: true
|
|
||||||
},
|
|
||||||
handler: (req, res) =>
|
|
||||||
nextApp
|
|
||||||
.getRequestHandler()(req.raw, res.raw)
|
|
||||||
.then(() => {
|
|
||||||
res.hijack();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
server.addHook("onClose", () => nextApp.close());
|
|
||||||
await nextApp.prepare();
|
|
||||||
/* eslint-enable */
|
|
||||||
}
|
|
||||||
};
|
|
64
backend/src/server/plugins/serve-ui.ts
Normal file
64
backend/src/server/plugins/serve-ui.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import staticServe from "@fastify/static";
|
||||||
|
|
||||||
|
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
// to enabled this u need to set standalone mode to true
|
||||||
|
export const registerServeUI = async (
|
||||||
|
server: FastifyZodProvider,
|
||||||
|
{
|
||||||
|
standaloneMode,
|
||||||
|
dir
|
||||||
|
}: {
|
||||||
|
standaloneMode?: boolean;
|
||||||
|
dir: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
// use this only for frontend runtime static non-sensitive configuration in standalone mode
|
||||||
|
// that app needs before loading like posthog dsn key
|
||||||
|
// for most of the other usecase use server config
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/runtime-ui-env.js",
|
||||||
|
schema: {
|
||||||
|
hide: true
|
||||||
|
},
|
||||||
|
handler: (_req, res) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
void res.type("application/javascript");
|
||||||
|
const config = {
|
||||||
|
CAPTCHA_SITE_KEY: appCfg.CAPTCHA_SITE_KEY,
|
||||||
|
POSTHOG_API_KEY: appCfg.POSTHOG_PROJECT_API_KEY,
|
||||||
|
INTERCOM_ID: appCfg.INTERCOM_ID,
|
||||||
|
TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED
|
||||||
|
};
|
||||||
|
const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`;
|
||||||
|
void res.send(js);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (standaloneMode) {
|
||||||
|
const frontendName = IS_PACKAGED ? "frontend" : "frontend-build";
|
||||||
|
const frontendPath = path.join(dir, frontendName);
|
||||||
|
await server.register(staticServe, {
|
||||||
|
root: frontendPath,
|
||||||
|
wildcard: false
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/*",
|
||||||
|
schema: {
|
||||||
|
hide: true
|
||||||
|
},
|
||||||
|
handler: (request, reply) => {
|
||||||
|
if (request.url.startsWith("/api")) {
|
||||||
|
reply.callNotFound();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void reply.sendFile("index.html");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@@ -75,6 +75,13 @@ import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-da
|
|||||||
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
|
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
|
||||||
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
||||||
import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
||||||
|
import { sshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { sshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { sshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||||
|
import { sshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
|
||||||
|
import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
|
import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
|
import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@@ -84,6 +91,8 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||||
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
|
import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||||
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||||
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||||
@@ -172,6 +181,7 @@ import { projectUserMembershipRoleDALFactory } from "@app/services/project-membe
|
|||||||
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||||
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
||||||
|
import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||||
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
import { secretQueueFactory } from "@app/services/secret/secret-queue";
|
import { secretQueueFactory } from "@app/services/secret/secret-queue";
|
||||||
import { secretServiceFactory } from "@app/services/secret/secret-service";
|
import { secretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
@@ -307,6 +317,7 @@ export const registerRoutes = async (
|
|||||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
const telemetryDAL = telemetryDALFactory(db);
|
const telemetryDAL = telemetryDALFactory(db);
|
||||||
|
const appConnectionDAL = appConnectionDALFactory(db);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDAL = permissionDALFactory(db);
|
const permissionDAL = permissionDALFactory(db);
|
||||||
@@ -345,6 +356,12 @@ export const registerRoutes = async (
|
|||||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||||
|
|
||||||
|
const sshCertificateDAL = sshCertificateDALFactory(db);
|
||||||
|
const sshCertificateBodyDAL = sshCertificateBodyDALFactory(db);
|
||||||
|
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
|
||||||
|
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
|
||||||
|
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
|
||||||
|
|
||||||
const kmsDAL = kmskeyDALFactory(db);
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
const externalKmsDAL = externalKmsDALFactory(db);
|
const externalKmsDAL = externalKmsDALFactory(db);
|
||||||
@@ -358,6 +375,7 @@ export const registerRoutes = async (
|
|||||||
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
||||||
|
|
||||||
const projectTemplateDAL = projectTemplateDALFactory(db);
|
const projectTemplateDAL = projectTemplateDALFactory(db);
|
||||||
|
const resourceMetadataDAL = resourceMetadataDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
@@ -538,7 +556,11 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const orgService = orgServiceFactory({
|
const orgService = orgServiceFactory({
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
|
queueService,
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -559,6 +581,7 @@ export const registerRoutes = async (
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
oidcConfigDAL,
|
oidcConfigDAL,
|
||||||
|
loginService,
|
||||||
projectBotService
|
projectBotService
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
@@ -707,6 +730,22 @@ export const registerRoutes = async (
|
|||||||
queueService
|
queueService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateBodyDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const sshCertificateTemplateService = sshCertificateTemplateServiceFactory({
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@@ -776,10 +815,59 @@ export const registerRoutes = async (
|
|||||||
projectTemplateDAL
|
projectTemplateDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
|
integrationAuthDAL,
|
||||||
|
integrationDAL,
|
||||||
|
permissionService,
|
||||||
|
projectBotService,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretQueueService = secretQueueFactory({
|
||||||
|
keyStore,
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL,
|
||||||
|
integrationAuthService,
|
||||||
|
projectBotService,
|
||||||
|
integrationDAL,
|
||||||
|
secretImportDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
webhookDAL,
|
||||||
|
orgDAL,
|
||||||
|
auditLogService,
|
||||||
|
userDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
smtpService,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
kmsService,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
secretVersionTagV2BridgeDAL,
|
||||||
|
secretRotationDAL,
|
||||||
|
integrationAuthDAL,
|
||||||
|
snapshotDAL,
|
||||||
|
snapshotSecretV2BridgeDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
|
orgService,
|
||||||
|
resourceMetadataDAL
|
||||||
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
queueService,
|
||||||
projectQueue: projectQueueService,
|
projectQueue: projectQueueService,
|
||||||
|
projectBotService,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
@@ -795,6 +883,9 @@ export const registerRoutes = async (
|
|||||||
certificateDAL,
|
certificateDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
@@ -859,48 +950,6 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const integrationAuthService = integrationAuthServiceFactory({
|
|
||||||
integrationAuthDAL,
|
|
||||||
integrationDAL,
|
|
||||||
permissionService,
|
|
||||||
projectBotService,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
const secretQueueService = secretQueueFactory({
|
|
||||||
keyStore,
|
|
||||||
queueService,
|
|
||||||
secretDAL,
|
|
||||||
folderDAL,
|
|
||||||
integrationAuthService,
|
|
||||||
projectBotService,
|
|
||||||
integrationDAL,
|
|
||||||
secretImportDAL,
|
|
||||||
projectEnvDAL,
|
|
||||||
webhookDAL,
|
|
||||||
orgDAL,
|
|
||||||
auditLogService,
|
|
||||||
userDAL,
|
|
||||||
projectMembershipDAL,
|
|
||||||
smtpService,
|
|
||||||
projectDAL,
|
|
||||||
projectBotDAL,
|
|
||||||
secretVersionDAL,
|
|
||||||
secretBlindIndexDAL,
|
|
||||||
secretTagDAL,
|
|
||||||
secretVersionTagDAL,
|
|
||||||
kmsService,
|
|
||||||
secretVersionV2BridgeDAL,
|
|
||||||
secretV2BridgeDAL,
|
|
||||||
secretVersionTagV2BridgeDAL,
|
|
||||||
secretRotationDAL,
|
|
||||||
integrationAuthDAL,
|
|
||||||
snapshotDAL,
|
|
||||||
snapshotSecretV2BridgeDAL,
|
|
||||||
secretApprovalRequestDAL,
|
|
||||||
projectKeyDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
|
||||||
orgService
|
|
||||||
});
|
|
||||||
const secretImportService = secretImportServiceFactory({
|
const secretImportService = secretImportServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
@@ -934,7 +983,8 @@ export const registerRoutes = async (
|
|||||||
secretApprovalPolicyService,
|
secretApprovalPolicyService,
|
||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
snapshotService
|
snapshotService,
|
||||||
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
|
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
|
||||||
@@ -961,7 +1011,8 @@ export const registerRoutes = async (
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectSlackConfigDAL
|
projectSlackConfigDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretService = secretServiceFactory({
|
const secretService = secretServiceFactory({
|
||||||
@@ -982,7 +1033,8 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
secretV2BridgeService,
|
secretV2BridgeService,
|
||||||
secretApprovalRequestService
|
secretApprovalRequestService,
|
||||||
|
licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretSharingService = secretSharingServiceFactory({
|
const secretSharingService = secretSharingServiceFactory({
|
||||||
@@ -1040,8 +1092,10 @@ export const registerRoutes = async (
|
|||||||
kmsService,
|
kmsService,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
|
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
|
||||||
secretVersionV2BridgeDAL
|
secretVersionV2BridgeDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretRotationQueue = secretRotationQueueFactory({
|
const secretRotationQueue = secretRotationQueueFactory({
|
||||||
telemetryService,
|
telemetryService,
|
||||||
secretRotationDAL,
|
secretRotationDAL,
|
||||||
@@ -1229,6 +1283,7 @@ export const registerRoutes = async (
|
|||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
|
secretDAL,
|
||||||
secretFolderVersionDAL: folderVersionDAL,
|
secretFolderVersionDAL: folderVersionDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
@@ -1292,7 +1347,8 @@ export const registerRoutes = async (
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
queueService,
|
queueService,
|
||||||
secretV2BridgeService
|
secretV2BridgeService,
|
||||||
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const migrationService = externalMigrationServiceFactory({
|
const migrationService = externalMigrationServiceFactory({
|
||||||
@@ -1308,6 +1364,13 @@ export const registerRoutes = async (
|
|||||||
externalGroupOrgRoleMappingDAL
|
externalGroupOrgRoleMappingDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appConnectionService = appConnectionServiceFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@@ -1376,6 +1439,8 @@ export const registerRoutes = async (
|
|||||||
auditLog: auditLogService,
|
auditLog: auditLogService,
|
||||||
auditLogStream: auditLogStreamService,
|
auditLogStream: auditLogStreamService,
|
||||||
certificate: certificateService,
|
certificate: certificateService,
|
||||||
|
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||||
|
sshCertificateTemplate: sshCertificateTemplateService,
|
||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
@@ -1402,7 +1467,8 @@ export const registerRoutes = async (
|
|||||||
migration: migrationService,
|
migration: migrationService,
|
||||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
||||||
projectTemplate: projectTemplateService,
|
projectTemplate: projectTemplateService,
|
||||||
totp: totpService
|
totp: totpService,
|
||||||
|
appConnection: appConnectionService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
@@ -0,0 +1,74 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||||
|
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
// can't use discriminated due to multiple schemas for certain apps
|
||||||
|
const SanitizedAppConnectionSchema = z.union([
|
||||||
|
...SanitizedAwsConnectionSchema.options,
|
||||||
|
...SanitizedGitHubConnectionSchema.options
|
||||||
|
]);
|
||||||
|
|
||||||
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
|
AwsConnectionListItemSchema,
|
||||||
|
GitHubConnectionListItemSchema
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/options",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List the available App Connection Options.",
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
appConnectionOptions: AppConnectionOptionsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: () => {
|
||||||
|
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions();
|
||||||
|
return { appConnectionOptions };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List all the App Connections for the current organization.",
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTIONS,
|
||||||
|
metadata: {
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnections };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -0,0 +1,274 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { startsWithVowel } from "@app/lib/fn";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
|
||||||
|
server,
|
||||||
|
app,
|
||||||
|
createSchema,
|
||||||
|
updateSchema,
|
||||||
|
responseSchema
|
||||||
|
}: {
|
||||||
|
app: AppConnection;
|
||||||
|
server: FastifyZodProvider;
|
||||||
|
createSchema: z.ZodType<{
|
||||||
|
name: string;
|
||||||
|
method: I["method"];
|
||||||
|
credentials: I["credentials"];
|
||||||
|
description?: string | null;
|
||||||
|
}>;
|
||||||
|
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
||||||
|
responseSchema: z.ZodTypeAny;
|
||||||
|
}) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `List the ${appName} Connections for the current organization.`,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnections: responseSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[];
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTIONS,
|
||||||
|
metadata: {
|
||||||
|
app,
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnections };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:connectionId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${appName} Connection by ID.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.findAppConnectionById(
|
||||||
|
app,
|
||||||
|
connectionId,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/name/:connectionName`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${appName} Connection by name.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionName: z
|
||||||
|
.string()
|
||||||
|
.min(0, "Connection name required")
|
||||||
|
.describe(AppConnections.GET_BY_NAME(app).connectionName)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionName } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.findAppConnectionByName(
|
||||||
|
app,
|
||||||
|
connectionName,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId: appConnection.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Create ${
|
||||||
|
startsWithVowel(appName) ? "an" : "a"
|
||||||
|
} ${appName} Connection for the current organization.`,
|
||||||
|
body: createSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { name, method, credentials, description } = req.body;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||||
|
{ name, method, app, credentials, description },
|
||||||
|
req.permission
|
||||||
|
)) as TAppConnection;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
method,
|
||||||
|
app,
|
||||||
|
connectionId: appConnection.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:connectionId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Update the specified ${appName} Connection.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.UPDATE(app).connectionId)
|
||||||
|
}),
|
||||||
|
body: updateSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { name, credentials, description } = req.body;
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.updateAppConnection(
|
||||||
|
{ name, credentials, connectionId, description },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
credentialsUpdated: Boolean(credentials),
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/:connectionId`,
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Delete the specified ${appName} Connection.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.deleteAppConnection(
|
||||||
|
app,
|
||||||
|
connectionId,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateAwsConnectionSchema,
|
||||||
|
SanitizedAwsConnectionSchema,
|
||||||
|
UpdateAwsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.AWS,
|
||||||
|
server,
|
||||||
|
responseSchema: SanitizedAwsConnectionSchema,
|
||||||
|
createSchema: CreateAwsConnectionSchema,
|
||||||
|
updateSchema: UpdateAwsConnectionSchema
|
||||||
|
});
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateGitHubConnectionSchema,
|
||||||
|
SanitizedGitHubConnectionSchema,
|
||||||
|
UpdateGitHubConnectionSchema
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.GitHub,
|
||||||
|
server,
|
||||||
|
responseSchema: SanitizedGitHubConnectionSchema,
|
||||||
|
createSchema: CreateGitHubConnectionSchema,
|
||||||
|
updateSchema: UpdateGitHubConnectionSchema
|
||||||
|
});
|
@@ -0,0 +1,8 @@
|
|||||||
|
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
|
||||||
|
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
|
||||||
|
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||||
|
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
||||||
|
};
|
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./app-connection-router";
|
||||||
|
export * from "./apps";
|
@@ -63,7 +63,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
token: z.string()
|
token: z.string(),
|
||||||
|
organizationId: z.string().optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -115,7 +116,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { token };
|
return { token, organizationId: decodedToken.organizationId };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -17,6 +17,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SecretTagsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
@@ -408,6 +410,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SecretTagsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
@@ -693,6 +696,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SecretTagsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
@@ -864,6 +868,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SecretTagsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { APP_CONNECTION_REGISTER_MAP, registerAppConnectionRouter } from "@app/server/routes/v1/app-connection-routers";
|
||||||
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
||||||
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
||||||
|
|
||||||
@@ -110,4 +111,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
await server.register(registerCmekRouter, { prefix: "/kms" });
|
||||||
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (appConnectionsRouter) => {
|
||||||
|
await appConnectionsRouter.register(registerAppConnectionRouter);
|
||||||
|
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_MAP)) {
|
||||||
|
await appConnectionsRouter.register(router, { prefix: `/${app}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prefix: "/app-connections" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@@ -16,6 +16,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
|
|
||||||
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
@@ -29,9 +30,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.extend({
|
organizations: sanitizedOrganizationSchema
|
||||||
orgAuthMethod: z.string()
|
.extend({
|
||||||
}).array()
|
orgAuthMethod: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -137,7 +137,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.default("false")
|
.default("false")
|
||||||
.transform((value) => value === "true"),
|
.transform((value) => value === "true"),
|
||||||
type: z.enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, "all"]).optional()
|
type: z
|
||||||
|
.enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, ProjectType.SSH, "all"])
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -331,12 +331,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
failureAsync: async () => {
|
failureAsync: async () => {
|
||||||
return res.redirect(appCfg.SITE_URL as string);
|
return res.redirect(appCfg.SITE_URL as string);
|
||||||
},
|
},
|
||||||
successAsync: async (installation) => {
|
successAsync: async () => {
|
||||||
const metadata = JSON.parse(installation.metadata || "") as {
|
return res.redirect(`${appCfg.SITE_URL}/organization/settings?selectedTab=workflow-integrations`);
|
||||||
orgId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.redirect(`${appCfg.SITE_URL}/org/${metadata.orgId}/settings?selectedTab=workflow-integrations`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -363,21 +364,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organization: OrganizationsSchema
|
organization: OrganizationsSchema,
|
||||||
|
accessToken: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
handler: async (req) => {
|
handler: async (req, res) => {
|
||||||
if (req.auth.actor !== ActorType.USER) return;
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
const organization = await server.services.org.deleteOrganizationById(
|
const cfg = getConfig();
|
||||||
req.permission.id,
|
|
||||||
req.params.organizationId,
|
const { organization, tokens } = await server.services.org.deleteOrganizationById({
|
||||||
req.permission.authMethod,
|
userId: req.permission.id,
|
||||||
req.permission.orgId
|
orgId: req.params.organizationId,
|
||||||
);
|
actorAuthMethod: req.permission.authMethod,
|
||||||
return { organization };
|
actorOrgId: req.permission.orgId,
|
||||||
|
authorizationHeader: req.headers.authorization,
|
||||||
|
userAgentHeader: req.headers["user-agent"],
|
||||||
|
ipAddress: req.realIp
|
||||||
|
});
|
||||||
|
|
||||||
|
void res.setCookie("jid", tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: cfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return { organization, accessToken: tokens.accessToken };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -10,6 +10,9 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||||
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
|
import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema";
|
||||||
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
@@ -500,4 +503,101 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { certificateTemplates };
|
return { certificateTemplates };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/ssh-certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().default(0).describe(PROJECTS.LIST_SSH_CERTIFICATES.offset),
|
||||||
|
limit: z.coerce.number().default(25).describe(PROJECTS.LIST_SSH_CERTIFICATES.limit)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificates: z.array(sanitizedSshCertificate),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificates, totalCount } = await server.services.project.listProjectSshCertificates({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
offset: req.query.offset,
|
||||||
|
limit: req.query.limit
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificates, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/ssh-certificate-templates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CERTIFICATE_TEMPLATES.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificateTemplates: z.array(sanitizedSshCertificateTemplate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplates } = await server.services.project.listProjectSshCertificateTemplates({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificateTemplates };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/ssh-cas",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
cas: z.array(sanitizedSshCa)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const cas = await server.services.project.listProjectSshCas({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { cas };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
|
|
||||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -134,7 +135,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Return organizations that current user is part of",
|
description: "Return organizations that current user is part of",
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.array()
|
organizations: sanitizedOrganizationSchema.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -18,6 +18,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
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 { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
|
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@@ -35,6 +36,12 @@ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReference
|
|||||||
children: z.lazy(() => SecretReferenceNodeTree.array())
|
children: z.lazy(() => SecretReferenceNodeTree.array())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SecretNameSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine((el) => !el.includes(" "), "Secret name cannot contain spaces.");
|
||||||
|
|
||||||
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -50,7 +57,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim().describe(SECRETS.ATTACH_TAGS.secretName)
|
secretName: SecretNameSchema.describe(SECRETS.ATTACH_TAGS.secretName)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug),
|
projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug),
|
||||||
@@ -113,7 +120,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim().describe(SECRETS.DETACH_TAGS.secretName)
|
secretName: z.string().describe(SECRETS.DETACH_TAGS.secretName)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectSlug: z.string().trim().describe(SECRETS.DETACH_TAGS.projectSlug),
|
projectSlug: z.string().trim().describe(SECRETS.DETACH_TAGS.projectSlug),
|
||||||
@@ -205,6 +212,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SecretTagsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
@@ -220,7 +228,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
folderId: z.string().optional(),
|
folderId: z.string().optional(),
|
||||||
secrets: secretRawSchema.omit({ createdAt: true, updatedAt: true }).array()
|
secrets: secretRawSchema
|
||||||
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
|
.extend({
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -348,7 +361,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.extend({ name: z.string() })
|
.extend({ name: z.string() })
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional(),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -434,7 +448,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim().describe(RAW_SECRETS.CREATE.secretName)
|
secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId),
|
workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId),
|
||||||
@@ -450,6 +464,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.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.CREATE.secretValue),
|
.describe(RAW_SECRETS.CREATE.secretValue),
|
||||||
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
|
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds),
|
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds),
|
||||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
|
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type),
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type),
|
||||||
@@ -484,6 +499,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretValue: req.body.secretValue,
|
secretValue: req.body.secretValue,
|
||||||
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
||||||
secretComment: req.body.secretComment,
|
secretComment: req.body.secretComment,
|
||||||
|
secretMetadata: req.body.secretMetadata,
|
||||||
tagIds: req.body.tagIds,
|
tagIds: req.body.tagIds,
|
||||||
secretReminderNote: req.body.secretReminderNote,
|
secretReminderNote: req.body.secretReminderNote,
|
||||||
secretReminderRepeatDays: req.body.secretReminderRepeatDays
|
secretReminderRepeatDays: req.body.secretReminderRepeatDays
|
||||||
@@ -539,7 +555,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName)
|
secretName: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId),
|
workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId),
|
||||||
@@ -558,13 +574,14 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type),
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type),
|
||||||
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(),
|
||||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||||
secretReminderRepeatDays: z
|
secretReminderRepeatDays: z
|
||||||
.number()
|
.number()
|
||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
.describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays),
|
.describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays),
|
||||||
newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName),
|
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
|
||||||
secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment)
|
secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -595,8 +612,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretReminderNote: req.body.secretReminderNote,
|
secretReminderNote: req.body.secretReminderNote,
|
||||||
metadata: req.body.metadata,
|
metadata: req.body.metadata,
|
||||||
newSecretName: req.body.newSecretName,
|
newSecretName: req.body.newSecretName,
|
||||||
secretComment: req.body.secretComment
|
secretComment: req.body.secretComment,
|
||||||
|
secretMetadata: req.body.secretMetadata
|
||||||
});
|
});
|
||||||
|
|
||||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||||
return { approval: secretOperation.approval };
|
return { approval: secretOperation.approval };
|
||||||
}
|
}
|
||||||
@@ -647,7 +666,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim().describe(RAW_SECRETS.DELETE.secretName)
|
secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId),
|
workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId),
|
||||||
@@ -1842,7 +1861,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(RAW_SECRETS.CREATE.secretPath),
|
.describe(RAW_SECRETS.CREATE.secretPath),
|
||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretKey: z.string().trim().describe(RAW_SECRETS.CREATE.secretName),
|
secretKey: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName),
|
||||||
secretValue: z
|
secretValue: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
||||||
@@ -1850,6 +1869,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
|
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
|
||||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
|
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
|
||||||
metadata: z.record(z.string()).optional(),
|
metadata: z.record(z.string()).optional(),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds)
|
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
@@ -1942,16 +1962,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(RAW_SECRETS.UPDATE.secretPath),
|
.describe(RAW_SECRETS.UPDATE.secretPath),
|
||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretKey: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName),
|
secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName),
|
||||||
secretValue: z
|
secretValue: z
|
||||||
.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),
|
||||||
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: z.string().min(1).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().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||||
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
secretReminderRepeatDays: z
|
secretReminderRepeatDays: z
|
||||||
.number()
|
.number()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -2047,7 +2068,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(RAW_SECRETS.DELETE.secretPath),
|
.describe(RAW_SECRETS.DELETE.secretPath),
|
||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretKey: z.string().trim().describe(RAW_SECRETS.DELETE.secretName),
|
secretKey: z.string().describe(RAW_SECRETS.DELETE.secretName),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared)
|
type: z.nativeEnum(SecretType).default(SecretType.Shared)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
|
11
backend/src/services/app-connection/app-connection-dal.ts
Normal file
11
backend/src/services/app-connection/app-connection-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 TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
|
||||||
|
|
||||||
|
export const appConnectionDALFactory = (db: TDbClient) => {
|
||||||
|
const appConnectionOrm = ormify(db, TableName.AppConnection);
|
||||||
|
|
||||||
|
return { ...appConnectionOrm };
|
||||||
|
};
|
@@ -0,0 +1,4 @@
|
|||||||
|
export enum AppConnection {
|
||||||
|
GitHub = "github",
|
||||||
|
AWS = "aws"
|
||||||
|
}
|
92
backend/src/services/app-connection/app-connection-fns.ts
Normal file
92
backend/src/services/app-connection/app-connection-fns.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||||
|
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||||
|
import {
|
||||||
|
AwsConnectionMethod,
|
||||||
|
getAwsAppConnectionListItem,
|
||||||
|
validateAwsConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
getGitHubConnectionListItem,
|
||||||
|
GitHubConnectionMethod,
|
||||||
|
validateGitHubConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
export const listAppConnectionOptions = () => {
|
||||||
|
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptAppConnectionCredentials = async ({
|
||||||
|
orgId,
|
||||||
|
credentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
credentials: TAppConnection["credentials"];
|
||||||
|
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||||
|
}) => {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||||
|
plainText: Buffer.from(JSON.stringify(credentials))
|
||||||
|
});
|
||||||
|
|
||||||
|
return encryptedCredentialsBlob;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptAppConnectionCredentials = async ({
|
||||||
|
orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
encryptedCredentials: Buffer;
|
||||||
|
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||||
|
}) => {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPlainTextBlob = decryptor({
|
||||||
|
cipherTextBlob: encryptedCredentials
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAppConnectionCredentials = async (
|
||||||
|
appConnection: TAppConnectionConfig
|
||||||
|
): Promise<TAppConnection["credentials"]> => {
|
||||||
|
const { app } = appConnection;
|
||||||
|
switch (app) {
|
||||||
|
case AppConnection.AWS: {
|
||||||
|
return validateAwsConnectionCredentials(appConnection);
|
||||||
|
}
|
||||||
|
case AppConnection.GitHub:
|
||||||
|
return validateGitHubConnectionCredentials(appConnection);
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled App Connection ${app}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||||
|
switch (method) {
|
||||||
|
case GitHubConnectionMethod.App:
|
||||||
|
return "GitHub App";
|
||||||
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
return "OAuth";
|
||||||
|
case AwsConnectionMethod.AccessKey:
|
||||||
|
return "Access Key";
|
||||||
|
case AwsConnectionMethod.AssumeRole:
|
||||||
|
return "Assume Role";
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,6 @@
|
|||||||
|
import { AppConnection } from "./app-connection-enums";
|
||||||
|
|
||||||
|
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||||
|
[AppConnection.AWS]: "AWS",
|
||||||
|
[AppConnection.GitHub]: "GitHub"
|
||||||
|
};
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
|
||||||
|
import { AppConnection } from "./app-connection-enums";
|
||||||
|
|
||||||
|
export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||||
|
encryptedCredentials: true,
|
||||||
|
app: true,
|
||||||
|
method: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(AppConnections.CREATE(app).description)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(AppConnections.UPDATE(app).description)
|
||||||
|
});
|
360
backend/src/services/app-connection/app-connection-service.ts
Normal file
360
backend/src/services/app-connection/app-connection-service.ts
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
decryptAppConnectionCredentials,
|
||||||
|
encryptAppConnectionCredentials,
|
||||||
|
getAppConnectionMethodName,
|
||||||
|
listAppConnectionOptions,
|
||||||
|
validateAppConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import {
|
||||||
|
TAppConnection,
|
||||||
|
TAppConnectionConfig,
|
||||||
|
TCreateAppConnectionDTO,
|
||||||
|
TUpdateAppConnectionDTO,
|
||||||
|
TValidateAppConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
||||||
|
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
|
|
||||||
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">; // TODO: remove once launched
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||||
|
|
||||||
|
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||||
|
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||||
|
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appConnectionServiceFactory = ({
|
||||||
|
appConnectionDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
licenseService
|
||||||
|
}: TAppConnectionServiceFactoryDep) => {
|
||||||
|
// app connections are disabled for public until launch
|
||||||
|
const checkAppServicesAvailability = async (orgId: string) => {
|
||||||
|
const subscription = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
|
if (!subscription.appConnections) throw new BadRequestError({ message: "App Connections are not available yet." });
|
||||||
|
};
|
||||||
|
|
||||||
|
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const appConnections = await appConnectionDAL.find(
|
||||||
|
app
|
||||||
|
? { orgId: actor.orgId, app }
|
||||||
|
: {
|
||||||
|
orgId: actor.orgId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
appConnections
|
||||||
|
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
||||||
|
.map(async ({ encryptedCredentials, ...connection }) => {
|
||||||
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService,
|
||||||
|
orgId: connection.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...connection,
|
||||||
|
credentials
|
||||||
|
} as TAppConnection;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAppConnectionById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...appConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId });
|
||||||
|
|
||||||
|
if (!appConnection)
|
||||||
|
throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...appConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAppConnection = async (
|
||||||
|
{ method, app, credentials, ...params }: TCreateAppConnectionDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await appConnectionDAL.findOne(
|
||||||
|
{
|
||||||
|
name: params.name,
|
||||||
|
orgId: actor.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `An App Connection with the name "${params.name}" already exists`
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
credentials,
|
||||||
|
method,
|
||||||
|
orgId: actor.orgId
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = await appConnectionDAL.create(
|
||||||
|
{
|
||||||
|
orgId: actor.orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
method,
|
||||||
|
app,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...connection,
|
||||||
|
credentials: validatedCredentials
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return appConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAppConnection = async (
|
||||||
|
{ connectionId, credentials, ...params }: TUpdateAppConnectionDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
|
if (params.name && appConnection.name !== params.name) {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await appConnectionDAL.findOne(
|
||||||
|
{
|
||||||
|
name: params.name,
|
||||||
|
orgId: appConnection.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `An App Connection with the name "${params.name}" already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let encryptedCredentials: undefined | Buffer;
|
||||||
|
|
||||||
|
if (credentials) {
|
||||||
|
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
||||||
|
method,
|
||||||
|
credentials
|
||||||
|
}).success
|
||||||
|
)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid credential format for ${
|
||||||
|
APP_CONNECTION_NAME_MAP[app]
|
||||||
|
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
credentials,
|
||||||
|
method
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
if (!validatedCredentials)
|
||||||
|
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||||
|
|
||||||
|
encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedConnection = await appConnectionDAL.updateById(
|
||||||
|
connectionId,
|
||||||
|
{
|
||||||
|
orgId: actor.orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedConnection;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedAppConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: updatedAppConnection.encryptedCredentials,
|
||||||
|
orgId: updatedAppConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||||
|
|
||||||
|
// TODO: specify delete error message if due to existing dependencies
|
||||||
|
|
||||||
|
const deletedAppConnection = await appConnectionDAL.deleteById(connectionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...deletedAppConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: deletedAppConnection.encryptedCredentials,
|
||||||
|
orgId: deletedAppConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listAppConnectionOptions,
|
||||||
|
listAppConnectionsByOrg,
|
||||||
|
findAppConnectionById,
|
||||||
|
findAppConnectionByName,
|
||||||
|
createAppConnection,
|
||||||
|
updateAppConnection,
|
||||||
|
deleteAppConnection
|
||||||
|
};
|
||||||
|
};
|
31
backend/src/services/app-connection/app-connection-types.ts
Normal file
31
backend/src/services/app-connection/app-connection-types.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
TAwsConnection,
|
||||||
|
TAwsConnectionConfig,
|
||||||
|
TAwsConnectionInput,
|
||||||
|
TValidateAwsConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
TGitHubConnection,
|
||||||
|
TGitHubConnectionConfig,
|
||||||
|
TGitHubConnectionInput,
|
||||||
|
TValidateGitHubConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
|
||||||
|
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
|
||||||
|
|
||||||
|
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
|
||||||
|
|
||||||
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
|
TAppConnectionInput,
|
||||||
|
"credentials" | "method" | "name" | "app" | "description"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
|
||||||
|
|
||||||
|
export type TValidateAppConnectionCredentials =
|
||||||
|
| TValidateAwsConnectionCredentials
|
||||||
|
| TValidateGitHubConnectionCredentials;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user