Compare commits

...

138 Commits

Author SHA1 Message Date
Scott Wilson
1f366fbe17 fix: update sync job status 2025-01-08 16:43:48 -08:00
Scott Wilson
41484239c6 chore: revert feature flag 2025-01-08 16:37:12 -08:00
Scott Wilson
06c1471f3f improvements: address feedback 2025-01-08 16:36:49 -08:00
Scott Wilson
2a987c61eb improvement: improve otel name and descriptions 2025-01-07 19:28:06 -08:00
Scott Wilson
e26a67d545 fix: uncomment delete from sync 2025-01-07 18:59:58 -08:00
Scott Wilson
487a679aa9 fix: remove reference to pg error to fix spec check 2025-01-07 17:36:44 -08:00
Scott Wilson
b57bdd869c feature(backend): secret sync backend 2025-01-07 17:16:51 -08:00
Maidul Islam
1576358805 Merge pull request #2947 from Infisical/auth0-saml
Add Support for Auth0 SAML
2025-01-07 15:04:44 -05:00
Scott Wilson
e6103d2d3f docs: update initial saml setup steps 2025-01-07 11:45:07 -08:00
Maidul Islam
8bf8bc77c9 Merge pull request #2951 from akhilmhdh/fix/broken-image
feat: resolved app not loading on org no access
2025-01-07 14:29:47 -05:00
=
3219723149 feat: resolved app not loading on org no access 2025-01-08 00:55:22 +05:30
Akhil Mohan
6d3793beff Merge pull request #2949 from akhilmhdh/fix/broken-image
Fixed broke image
2025-01-08 00:18:38 +05:30
Scott Wilson
0df41f3391 Merge pull request #2948 from Infisical/fix-user-groups-plan
Improvement: Clarify Enterprise Plan for User Group Feature Upgrade Modal
2025-01-07 10:44:32 -08:00
=
1acac9d479 feat: resolved broken gitlab image and hidden the standalone endpoints from api documentation 2025-01-08 00:14:31 +05:30
Tuan Dang
0cefd6f837 Run linter 2025-01-08 01:41:22 +07:00
Scott Wilson
5e9dc0b98d clarify enterprise plan for user group feature upgrade modal 2025-01-07 10:38:09 -08:00
Tuan Dang
f632847dc6 Merge branch 'main', remote-tracking branch 'origin' into auth0-saml 2025-01-08 01:35:11 +07:00
Tuan Dang
faa6d1cf40 Add support for Auth0 SAML 2025-01-08 01:34:03 +07:00
Maidul Islam
7fb18870e3 Merge pull request #2946 from akhilmhdh/feat/updated-saml-error-message
Updated saml error message
2025-01-07 10:57:03 -05:00
Maidul Islam
ae841715e5 update saml error message 2025-01-07 10:56:44 -05:00
=
baac87c16a feat: updated saml error message on missing email or first name attribute 2025-01-07 21:17:25 +05:30
Akhil Mohan
b726187ba3 Merge pull request #2917 from mr-ssd/patch-1
docs: add note for dynamic secrets as paid feature
2025-01-07 14:12:50 +05:30
Akhil Mohan
d98ff32b07 Merge pull request #2919 from mr-ssd/patch-2
docs: add note Approval Workflows as paid feature
2025-01-07 14:12:12 +05:30
Sheen
1fa510b32f Merge pull request #2944 from Infisical/feat/target-specific-azure-key-vault-tenant
feat: target specific azure key vault tenant
2025-01-07 14:05:14 +08:00
Maidul Islam
c57f0d8120 Merge pull request #2945 from Infisical/misc/made-secret-path-input-show-correct-folder
misc: made secret path input show correct folders
2025-01-06 23:28:36 -05:00
Sheen Capadngan
00490f2cff misc: made secret path input show correct folders based on env in integrations 2025-01-07 12:08:09 +08:00
Sheen Capadngan
ee58f538c0 feat: target specific azure key vault tenant 2025-01-07 11:53:17 +08:00
Maidul Islam
0fa20f7839 Merge pull request #2943 from Infisical/aws-sm-integration-force-delete
Fix: Set Force Flag on Delete Secret Command for AWS Secrets Manager
2025-01-06 20:51:48 -05:00
Scott Wilson
40ef75d3bd fix: use force flag when deleting secrets from aws secret manager integration 2025-01-06 16:11:32 -08:00
Maidul Islam
26af13453c Merge pull request #2942 from akhilmhdh/fix/text-typo
fix: resolved typo in layout
2025-01-06 17:31:06 -05:00
=
ad1f71883d fix: resolved typo in layout 2025-01-07 02:58:56 +05:30
Maidul Islam
2659ea7170 Merge pull request #2940 from Infisical/misc/updated-openssh-installation-for-fips
misc: updated openssh installation for fips
2025-01-06 14:08:07 -05:00
Sheen Capadngan
d2e3f504fd misc: updated openssh installation for fips 2025-01-07 03:04:57 +08:00
Maidul Islam
ca4151a34d Merge pull request #2935 from akhilmhdh/refactor/rbr
Adios nextjs
2025-01-06 14:01:22 -05:00
=
d4bc104fd1 feat: adios nextjs 2025-01-06 18:38:57 +00:00
=
7e3a3fcdb0 feat: updated the installation id to coerce string 2025-01-06 23:37:16 +05:30
=
7f67912d2f feat: resolved failing be lint check 2025-01-06 22:18:49 +05:30
=
a7f4020c08 feat: bumped nodejs from 16 to 20 2025-01-06 22:13:56 +05:30
=
d2d89034ba feat: moved more of isLoading to isPending 2025-01-06 22:13:16 +05:30
=
2fc6c564c0 feat: removed all existBeforeEnter error 2025-01-06 19:56:53 +05:30
=
240b86c1d5 fix: resolved project list issue and app connection github not listed 2025-01-06 19:48:50 +05:30
=
30d66cef89 feat: resolved slack failing and approval url correction 2025-01-06 19:18:10 +05:30
=
b7d11444a9 feat: resolved bug on org change select and project on change 2025-01-06 19:01:10 +05:30
=
0a6aef0afa feat: lint fixes 2025-01-06 14:56:51 +05:30
=
0ac7ec460b feat: resolving some bugs 2025-01-06 14:56:50 +05:30
=
808a901aee feat: updated env in docker files 2025-01-06 14:56:50 +05:30
=
d5412f916f feat: updated frontend use run time config inject value 2025-01-06 14:56:50 +05:30
=
d0648ca596 feat: added runtime env config endpoint 2025-01-06 14:56:50 +05:30
=
3291f1f908 feat: resolved typo in integration redirects 2025-01-06 14:56:50 +05:30
=
965d30bd03 feat: updated docker 2025-01-06 14:56:50 +05:30
=
68fe7c589a feat: updated backend to support bare react as standalone 2025-01-06 14:56:50 +05:30
=
54377a44d3 feat: renamed old frontend and fixed some minor bugs 2025-01-06 14:56:49 +05:30
=
8c902d7699 feat: added error page and not found page handler 2025-01-06 14:55:14 +05:30
=
c25c84aeb3 feat: added csp and minor changes 2025-01-06 14:55:14 +05:30
=
4359eb7313 feat: testing all todo comments and cli 2025-01-06 14:55:13 +05:30
=
322536d738 feat: completed migration of all integrations pages 2025-01-06 14:55:13 +05:30
=
6c5db3a187 feat: completed secret manager integration list and detail page 2025-01-06 14:55:13 +05:30
=
a337e6badd feat: bug fixes in overview page and dashboard page 2025-01-06 14:55:13 +05:30
=
524a97e9a6 fix: resolved infinite rendering issue caused by zustand 2025-01-06 14:55:13 +05:30
=
c56f598115 feat: switched to official lottie react and adjusted all the existings ones 2025-01-06 14:55:13 +05:30
=
19d32a1a3b feat: completed migration of ssh product 2025-01-06 14:55:12 +05:30
=
7e5417a0eb feat: completed migration of app connection 2025-01-06 14:55:12 +05:30
=
afd6de27fe feat: re-arranged route paths 2025-01-06 14:55:12 +05:30
=
7781a6b7e7 feat: added static images and fixing minor layout issues 2025-01-06 14:55:12 +05:30
=
b3b4e41d92 feat: resolved ico header, failing translation and lottie icon issues 2025-01-06 14:55:12 +05:30
=
5225f5136a feat: resolving issues on loader 2025-01-06 14:55:12 +05:30
=
398adfaf76 feat: resolved all ts issues 2025-01-06 14:55:12 +05:30
=
d77c26fa38 feat: resolved almost all ts issues 2025-01-06 14:55:11 +05:30
=
ef7b81734a feat: added kms routes 2025-01-06 14:55:11 +05:30
=
09b489a348 feat: completed cert routes 2025-01-06 14:55:11 +05:30
=
6b5c50def0 feat: added secret-manager routes 2025-01-06 14:55:11 +05:30
=
1f2d52176c feat: added org route 2025-01-06 14:55:11 +05:30
=
7002e297c8 feat: removed routes and added kms, cert to pages setup 2025-01-06 14:55:11 +05:30
=
71864a131f feat: changed to virtual route for secret-manager 2025-01-06 14:55:11 +05:30
=
9964d2ecaa feat: completed switch to virtual for org pages 2025-01-06 14:55:10 +05:30
=
3ebbaefc2a feat: changed to virtual route for auth and personal settings 2025-01-06 14:55:10 +05:30
=
dd5c494bdb feat: secret manager routes migration completed 2025-01-06 14:55:10 +05:30
=
bace8af5a1 feat: removed $organizationId from the routes 2025-01-06 14:55:10 +05:30
=
f56196b820 feat: completed project layout base 2025-01-06 14:55:10 +05:30
=
7042d73410 feat: upgrade react-query to v5 and changed hooks to staletime inf for prev context based ones 2025-01-06 14:55:09 +05:30
=
cb22ee315e feat: resolved a lot of ts issues, planning to migrate to v5 tanstack query 2025-01-06 14:55:09 +05:30
=
701eb7cfc6 feat: resolved breaking dev 2025-01-06 14:55:09 +05:30
=
bf8df14b01 feat: added hoc, resolved dropdown type issue 2025-01-06 14:55:09 +05:30
=
1ba8b6394b feat: added virtual route to mount the layout without wrapping again 2025-01-06 14:55:09 +05:30
=
c442c8483a feat: completed signup and personal settings ui 2025-01-06 14:55:09 +05:30
=
0435305a68 feat: resolved eslint issues and seperated org layout to make it simple 2025-01-06 14:55:09 +05:30
=
febf11f502 feat: added organization layout base with subscription and user loading 2025-01-06 14:55:08 +05:30
=
64fd15c32d feat: modified backend token endpoint to return organizationid 2025-01-06 14:55:08 +05:30
=
a2c9494d52 feat: added signup routes and moved server config to router context 2025-01-06 14:55:08 +05:30
=
18460e0678 feat: base for signup pages completed 2025-01-06 14:55:08 +05:30
=
3d03fece74 feat: first login page base completed 2025-01-06 14:55:08 +05:30
=
234e7eb9be feat: added ui components 2025-01-06 14:55:08 +05:30
=
04af313bf0 feat: added all old dependencies 2025-01-06 14:55:08 +05:30
=
9b038ccc45 feat: added tanstack router 2025-01-06 14:55:07 +05:30
=
9beb384546 feat: added tailwindcss with prettier tailwind support 2025-01-06 14:55:07 +05:30
=
12ec9b4b4e feat: added type check, lint and prettier 2025-01-06 14:55:07 +05:30
Maidul Islam
96b8e7fda8 Merge pull request #2930 from Infisical/vmatsiiako-wiki-patch-1 2025-01-01 18:12:48 -05:00
Vlad Matsiiako
93b9108aa3 Update onboarding.mdx 2025-01-01 15:04:50 -08:00
Sheen
99017ea1ae Merge pull request #2923 from Infisical/akhilmhdh-patch-azure
fix: resolved azure app config api not pulling all keys
2024-12-30 23:19:54 +08:00
Akhil Mohan
f32588112e fix: resolved azure app config api not pulling all keys 2024-12-29 19:51:59 +05:30
Maidul Islam
f9b0b6700a Merge pull request #2922 from Infisical/feat/authenticate-key-vault-against-specific-tenant
feat: auth key vault against specific tenant
2024-12-29 08:18:52 -05:00
Maidul Islam
b45d9398f0 Merge pull request #2920 from Infisical/vmatsiiako-intercom-patch-1 2024-12-27 21:47:36 -05:00
Maidul Islam
1d1140237f Update azure-app-configuration.mdx 2024-12-27 00:59:49 -05:00
Maidul Islam
937560fd8d Update azure-app-configuration.mdx 2024-12-27 00:58:48 -05:00
Maidul Islam
5f4b7b9ea7 Merge pull request #2921 from Infisical/patch-azure-label
Azure App Config Label patch
2024-12-27 00:40:42 -05:00
Maidul Islam
05139820a5 add docs for label and refereces 2024-12-26 16:36:27 -05:00
Sheen Capadngan
7f6bc3ecfe misc: added additional note for azure app config 2024-12-26 19:07:45 +08:00
Sheen Capadngan
d8cc000ad1 feat: authenticate key vault against specific tenant 2024-12-26 19:07:16 +08:00
Maidul Islam
8fc03c06d9 handle empty label 2024-12-26 01:17:47 -05:00
Vlad Matsiiako
50ceedf39f Remove intercom from docs 2024-12-25 16:40:45 -08:00
BlackMagiq
550096e72b Merge pull request #2787 from Mhammad-riyaz/riyaz/query
Fix Error When Clicking on CA Table Row After Viewing Certificate in Certificate Authorities Tab
2024-12-25 02:05:27 -08:00
Sida Say
1190ca2d77 docs: add note Approval Workflows as paid feature 2024-12-25 15:27:38 +07:00
Sida Say
2fb60201bc add not for dynamic secrets as paid feature 2024-12-25 14:17:27 +07:00
Maidul Islam
634b500244 Merge pull request #2907 from Infisical/temp-hide-app-connection-docs 2024-12-20 18:53:20 -05:00
Scott Wilson
54b4d4ae55 docs: temp hide app connections 2024-12-20 15:07:23 -08:00
BlackMagiq
2f6dab3f63 Merge pull request #2901 from Infisical/ssh-cli-docs
Documentation for SSH Command in CLI
2024-12-20 08:49:38 -08:00
Tuan Dang
e9564f5231 Fix ssh cli docs based on review 2024-12-19 22:12:32 -08:00
Tuan Dang
05cdca9202 Add docs for SSH CLI 2024-12-19 19:47:24 -08:00
BlackMagiq
5ab0c66dee Merge pull request #2898 from Infisical/cli-ssh-agent
Add SSH CLI capability to load issued SSH credentials into SSH Agent via addToAgent flag
2024-12-19 11:58:02 -08:00
Maidul Islam
2843818395 Merge pull request #2899 from Infisical/misc/add-custom-metrics-for-errors
misc: added metric for api errors
2024-12-19 14:15:13 -05:00
Sheen Capadngan
2357f3bc1f misc: added integration ID 2024-12-20 03:10:59 +08:00
Sheen Capadngan
cde813aafb misc: added custom metrics for integration syncs 2024-12-20 03:08:55 +08:00
Sheen Capadngan
bbc8091d44 misc: added metric for api errors 2024-12-20 02:37:44 +08:00
Daniel Hougaard
ce5e591457 Merge pull request #2895 from Infisical/daniel/vercel-integration-bug
fix(vercel-integration): vercel integration initial sync behavior
2024-12-19 19:05:31 +01:00
Daniel Hougaard
5ae74f9761 Update create.tsx 2024-12-19 18:53:17 +01:00
Scott Wilson
eef331bbd1 Merge pull request #2870 from Infisical/app-connections
Feat: App Connections
2024-12-19 09:51:38 -08:00
Scott Wilson
d5c2e9236a fix: doc typo 2024-12-19 09:43:14 -08:00
Scott Wilson
13eef7e524 Merge pull request #2896 from Infisical/role-description-schema-fix
Fix: Correct Role Description Schema to Accept Null
2024-12-19 07:09:52 -08:00
Scott Wilson
3fa84c578c fix: correct role description schema to accept null 2024-12-18 22:15:40 -08:00
Scott Wilson
c22ed04733 fix: correct imports to use alias 2024-12-18 21:30:36 -08:00
Scott Wilson
64fac1979f revert: mint and license 2024-12-18 21:21:28 -08:00
Scott Wilson
2d60f389c2 improvements: address feedback 2024-12-18 21:18:38 -08:00
Daniel Hougaard
7798e5a2ad fix: default behavior 2024-12-19 01:25:29 +01:00
Daniel Hougaard
ed78227725 fix(vercel-integration): initial sync logic 2024-12-19 01:18:47 +01:00
Scott Wilson
62968c5e43 merge main 2024-12-18 10:09:23 -08:00
Scott Wilson
7947e73569 fix: correct import path to use alias 2024-12-17 09:21:23 -08:00
Scott Wilson
52ce90846a feature: app connections 2024-12-16 22:46:08 -08:00
mohammad riyaz
242595fceb changed getCaCerts key from ca-cert -> ca-certs 2024-11-24 21:05:39 +05:30
1582 changed files with 42052 additions and 42330 deletions

View File

@@ -88,3 +88,20 @@ PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
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=

View File

@@ -18,18 +18,18 @@ jobs:
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Setup Node 16
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "20"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: 📦 Install dependencies
run: npm install
working-directory: frontend
- name: 🏗️ Run Type check
run: npm run type:check
run: npm run type:check
working-directory: frontend
- name: 🏗️ Run Link check
run: npm run lint:fix
run: npm run lint:fix
working-directory: frontend

View File

@@ -8,7 +8,7 @@ FROM node:20-slim AS base
FROM base AS frontend-dependencies
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
COPY frontend/package.json frontend/package-lock.json ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
@@ -23,17 +23,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ENV VITE_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ENV VITE_INTERCOM_ID $INTERCOM_ID
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
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# 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 mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
@@ -137,7 +126,7 @@ RUN apt-get update && apt-get install -y \
freetds-dev \
freetds-bin \
tdsodbc \
openssh \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Configure ODBC in production
@@ -160,14 +149,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ENV INTERCOM_ID=$INTERCOM_ID
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
WORKDIR /
@@ -192,4 +178,4 @@ EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]
CMD ["./standalone-entrypoint.sh"]

View File

@@ -12,7 +12,7 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
COPY frontend/package.json frontend/package-lock.json ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
@@ -27,17 +27,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ENV VITE_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ENV VITE_INTERCOM_ID $INTERCOM_ID
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
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
@@ -49,20 +48,10 @@ WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 non-root-user
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
@@ -159,14 +148,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ENV INTERCOM_ID=$INTERCOM_ID
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
COPY --from=backend-runner /app /backend
@@ -189,4 +175,4 @@ EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]
CMD ["./standalone-entrypoint.sh"]

View File

@@ -26,6 +26,7 @@
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/static": "^7.0.4",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",
@@ -5406,6 +5407,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
"integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==",
"license": "MIT",
"engines": {
"node": ">=14"
}
@@ -5545,6 +5547,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
"integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==",
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.1",
"escape-html": "~1.0.3",
@@ -5563,16 +5566,85 @@
}
},
"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==",
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz",
"integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==",
"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"
"fastq": "^1.17.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": {
@@ -5599,6 +5671,20 @@
"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": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
@@ -6062,9 +6148,10 @@
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
},
"node_modules/@lukeed/ms": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz",
"integrity": "sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -13879,9 +13966,9 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@@ -13903,7 +13990,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -13918,6 +14005,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express-session": {
@@ -17388,15 +17479,16 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -18383,9 +18475,9 @@
"license": "ISC"
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/path-type": {

View File

@@ -134,6 +134,7 @@
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/static": "^7.0.4",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",

View File

@@ -36,6 +36,7 @@ import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-cert
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
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 { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
@@ -79,6 +80,7 @@ import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
import { TSecretReplicationServiceFactory } from "@app/services/secret-replication/secret-replication-service";
import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { TSecretSyncServiceFactory } from "@app/services/secret-sync/secret-sync-service";
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
@@ -208,6 +210,8 @@ declare module "fastify" {
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
projectTemplate: TProjectTemplateServiceFactory;
totp: TTotpServiceFactory;
appConnection: TAppConnectionServiceFactory;
secretSync: TSecretSyncServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@@ -363,11 +363,13 @@ import {
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections";
import {
TExternalGroupOrgRoleMappings,
TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate
} from "@app/db/schemas/external-group-org-role-mappings";
import { TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate } from "@app/db/schemas/secret-syncs";
import {
TSecretV2TagJunction,
TSecretV2TagJunctionInsert,
@@ -886,5 +888,11 @@ declare module "knex/types/tables" {
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate
>;
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
TAppConnections,
TAppConnectionsInsert,
TAppConnectionsUpdate
>;
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
}
}

View 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);
}

View File

@@ -0,0 +1,46 @@
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.SecretSync))) {
await knex.schema.createTable(TableName.SecretSync, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description");
t.string("destination").notNullable();
t.boolean("isEnabled").notNullable().defaultTo(true);
t.integer("version").defaultTo(1).notNullable();
t.jsonb("destinationConfig").notNullable();
t.jsonb("syncOptions").notNullable();
t.uuid("folderId").notNullable();
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
t.uuid("connectionId").notNullable();
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
t.timestamps(true, true, true);
// sync
t.string("syncStatus");
t.string("lastSyncJobId");
t.string("lastSyncMessage");
t.datetime("lastSyncedAt");
// import
t.string("importStatus");
t.string("lastImportJobId");
t.string("lastImportMessage");
t.datetime("lastImportedAt");
// erase
t.string("eraseStatus");
t.string("lastEraseJobId");
t.string("lastEraseMessage");
t.datetime("lastErasedAt");
});
await createOnUpdateTrigger(knex, TableName.SecretSync);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SecretSync);
await dropOnUpdateTrigger(knex, TableName.SecretSync);
}

View 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>>;

View File

@@ -129,7 +129,9 @@ export enum TableName {
KmsKeyVersion = "kms_key_versions",
WorkflowIntegrations = "workflow_integrations",
SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs"
ProjectSlackConfigs = "project_slack_configs",
AppConnection = "app_connections",
SecretSync = "secret_syncs"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@@ -0,0 +1,39 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretSyncsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
destination: z.string(),
isEnabled: z.boolean().default(true),
version: z.number().default(1),
destinationConfig: z.unknown(),
syncOptions: z.unknown(),
folderId: z.string().uuid(),
connectionId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
syncStatus: z.string().nullable().optional(),
lastSyncJobId: z.string().nullable().optional(),
lastSyncMessage: z.string().nullable().optional(),
lastSyncedAt: z.date().nullable().optional(),
importStatus: z.string().nullable().optional(),
lastImportJobId: z.string().nullable().optional(),
lastImportMessage: z.string().nullable().optional(),
lastImportedAt: z.date().nullable().optional(),
eraseStatus: z.string().nullable().optional(),
lastEraseJobId: z.string().nullable().optional(),
lastEraseMessage: z.string().nullable().optional(),
lastErasedAt: z.date().nullable().optional()
});
export type TSecretSyncs = z.infer<typeof SecretSyncsSchema>;
export type TSecretSyncsInsert = Omit<z.input<typeof SecretSyncsSchema>, TImmutableDBKeys>;
export type TSecretSyncsUpdate = Partial<Omit<z.input<typeof SecretSyncsSchema>, TImmutableDBKeys>>;

View File

@@ -1,6 +1,7 @@
import { z } from "zod";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { OrgPermissionSchema } from "@app/ee/services/permission/org-permission";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -23,8 +24,8 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
"Please choose a different slug, the slug you have entered is reserved"
),
name: z.string().trim(),
description: z.string().trim().optional(),
permissions: z.any().array()
description: z.string().trim().nullish(),
permissions: OrgPermissionSchema.array()
}),
response: {
200: z.object({
@@ -95,8 +96,8 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
)
.optional(),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
permissions: z.any().array().optional()
description: z.string().trim().nullish(),
permissions: OrgPermissionSchema.array().optional()
}),
response: {
200: z.object({

View File

@@ -39,7 +39,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
)
.describe(PROJECT_ROLE.CREATE.slug),
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)
}),
response: {
@@ -95,7 +95,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.UPDATE.slug)
.optional(),
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()
}),
response: {

View File

@@ -84,7 +84,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
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;
}
@@ -123,7 +126,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
`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 || {})

View File

@@ -36,7 +36,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
)
.describe(PROJECT_ROLE.CREATE.slug),
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)
}),
response: {
@@ -91,7 +91,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.optional()
.describe(PROJECT_ROLE.UPDATE.slug),
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()
}),
response: {

View File

@@ -213,7 +213,7 @@ export const accessApprovalRequestServiceFactory = ({
);
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({
projectId: project.id,

View File

@@ -79,7 +79,8 @@ export const auditLogServiceFactory = ({
}
// add all cases in which project id or org id cannot be added
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
if (!data.projectId && !data.orgId)
throw new BadRequestError({ message: "Must specify either project id or org id" });
}
return auditLogQueue.pushToLog(data);

View File

@@ -6,11 +6,20 @@ import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-a
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
TCreateSecretSyncDTO,
TDeleteSecretSyncDTO,
TSecretSyncRaw,
TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types";
export type TListProjectAuditLogDTO = {
filter: {
@@ -222,7 +231,21 @@ export enum EventType {
CREATE_PROJECT_TEMPLATE = "create-project-template",
UPDATE_PROJECT_TEMPLATE = "update-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_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details",
GET_APP_CONNECTION = "get-app-connection",
CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection",
GET_SECRET_SYNCS = "get-secret-syncs",
GET_SECRET_SYNC = "get-secret-sync",
CREATE_SECRET_SYNC = "create-secret-sync",
UPDATE_SECRET_SYNC = "update-secret-sync",
DELETE_SECRET_SYNC = "delete-secret-sync",
SYNC_SECRET_SYNC = "sync-secret-sync",
IMPORT_SECRET_SYNC = "import-secret-sync",
ERASE_SECRET_SYNC = "erase-secret-sync"
}
interface UserActorMetadata {
@@ -1867,6 +1890,119 @@ interface ApplyProjectTemplateEvent {
};
}
interface GetAppConnectionsEvent {
type: EventType.GET_APP_CONNECTIONS;
metadata: {
app?: AppConnection;
count: number;
connectionIds: string[];
};
}
interface GetAvailableAppConnectionsDetailsEvent {
type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS;
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;
};
}
interface GetSecretSyncsEvent {
type: EventType.GET_SECRET_SYNCS;
metadata: {
destination?: SecretSync;
count: number;
syncIds: string[];
};
}
interface GetSecretSyncEvent {
type: EventType.GET_SECRET_SYNC;
metadata: {
destination: SecretSync;
syncId: string;
};
}
interface CreateSecretSyncEvent {
type: EventType.CREATE_SECRET_SYNC;
metadata: TCreateSecretSyncDTO & { syncId: string };
}
interface UpdateSecretSyncEvent {
type: EventType.UPDATE_SECRET_SYNC;
metadata: TUpdateSecretSyncDTO;
}
interface DeleteSecretSyncEvent {
type: EventType.DELETE_SECRET_SYNC;
metadata: TDeleteSecretSyncDTO;
}
interface SyncSecretSyncEvent {
type: EventType.SYNC_SECRET_SYNC;
metadata: Pick<
TSecretSyncRaw,
"syncOptions" | "destinationConfig" | "destination" | "syncStatus" | "environment" | "connectionId" | "folderId"
> & {
syncId: string;
syncMessage: string | null;
jobId: string;
jobRanAt: Date;
};
}
interface ImportSecretSyncEvent {
type: EventType.IMPORT_SECRET_SYNC;
metadata: Pick<
TSecretSyncRaw,
"syncOptions" | "destinationConfig" | "destination" | "importStatus" | "environment" | "connectionId" | "folderId"
> & {
syncId: string;
importMessage: string | null;
jobId: string;
jobRanAt: Date;
};
}
interface EraseSecretSyncEvent {
type: EventType.ERASE_SECRET_SYNC;
metadata: Pick<
TSecretSyncRaw,
"syncOptions" | "destinationConfig" | "destination" | "eraseStatus" | "environment" | "connectionId" | "folderId"
> & {
syncId: string;
eraseMessage: string | null;
jobId: string;
jobRanAt: Date;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -2038,4 +2174,18 @@ export type Event =
| CreateProjectTemplateEvent
| UpdateProjectTemplateEvent
| DeleteProjectTemplateEvent
| ApplyProjectTemplateEvent;
| ApplyProjectTemplateEvent
| GetAppConnectionsEvent
| GetAvailableAppConnectionsDetailsEvent
| GetAppConnectionEvent
| CreateAppConnectionEvent
| UpdateAppConnectionEvent
| DeleteAppConnectionEvent
| GetSecretSyncsEvent
| GetSecretSyncEvent
| CreateSecretSyncEvent
| UpdateSecretSyncEvent
| DeleteSecretSyncEvent
| SyncSecretSyncEvent
| ImportSecretSyncEvent
| EraseSecretSyncEvent;

View File

@@ -49,7 +49,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
},
pkiEst: false,
enforceMfa: false,
projectTemplates: false
projectTemplates: false,
appConnections: false
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@@ -246,8 +246,7 @@ export const licenseServiceFactory = ({
};
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const plan = await getPlan(orgId, projectId);
return plan;
};

View File

@@ -67,6 +67,7 @@ export type TFeatureSet = {
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: false;
appConnections: false; // TODO: remove once live
};
export type TOrgPlansTableDTO = {

View File

@@ -1,4 +1,12 @@
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import {
CASL_ACTION_SCHEMA_ENUM,
CASL_ACTION_SCHEMA_NATIVE_ENUM
} from "@app/ee/services/permission/permission-schemas";
import { PermissionConditionSchema } from "@app/ee/services/permission/permission-types";
import { PermissionConditionOperators } from "@app/lib/casl";
export enum OrgPermissionActions {
Read = "read",
@@ -7,6 +15,14 @@ export enum OrgPermissionActions {
Delete = "delete"
}
export enum OrgPermissionAppConnectionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
Connect = "connect"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
@@ -27,9 +43,14 @@ export enum OrgPermissionSubjects {
Kms = "kms",
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates"
ProjectTemplates = "project-templates",
AppConnections = "app-connections"
}
export type AppConnectionSubjectFields = {
connectionId: string;
};
export type OrgPermissionSet =
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions, OrgPermissionSubjects.Role]
@@ -46,8 +67,109 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [
OrgPermissionAppConnectionActions,
(
| OrgPermissionSubjects.AppConnections
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const AppConnectionConditionSchema = z
.object({
connectionId: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
export const OrgPermissionSchema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(OrgPermissionSubjects.Workspace).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Settings).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.IncidentAccount).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Sso).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Groups).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.SecretScanning).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Billing).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Identity).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Kms).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.AuditLogs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.ProjectTemplates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.AppConnections).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAppConnectionActions).describe(
"Describe what action an entity can take."
),
conditions: AppConnectionConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(OrgPermissionSubjects.AdminConsole).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe(
"Describe what action an entity can take."
)
})
]);
const buildAdminPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions
@@ -123,6 +245,17 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
can(
[
OrgPermissionAppConnectionActions.Create,
OrgPermissionAppConnectionActions.Edit,
OrgPermissionAppConnectionActions.Delete,
OrgPermissionAppConnectionActions.Read,
OrgPermissionAppConnectionActions.Connect
],
OrgPermissionSubjects.AppConnections
);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return rules;
@@ -153,6 +286,8 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
return rules;
};

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
z
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
.transform((el) => (typeof el === "string" ? [el] : el));
export const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));

View File

@@ -1,6 +1,10 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import {
CASL_ACTION_SCHEMA_ENUM,
CASL_ACTION_SCHEMA_NATIVE_ENUM
} from "@app/ee/services/permission/permission-schemas";
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
@@ -60,7 +64,8 @@ export enum ProjectPermissionSub {
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms",
Cmek = "cmek"
Cmek = "cmek",
SecretSyncs = "secret-syncs"
}
export type SecretSubjectFields = {
@@ -140,6 +145,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
@@ -147,14 +153,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
z
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
.transform((el) => (typeof el === "string" ? [el] : el));
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
// akhilmhdh: don't modify this for v2
// if you want to update create a new schema
const SecretConditionV1Schema = z
@@ -392,10 +390,15 @@ const GeneralPermissionSchema = [
}),
z.object({
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
})
];
@@ -511,7 +514,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshCertificateTemplates
ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SecretSyncs
].forEach((el) => {
can(
[
@@ -713,6 +717,16 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretSyncs
);
return rules;
};
@@ -746,6 +760,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
return rules;
};

View File

@@ -6,7 +6,8 @@ export enum SamlProviders {
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml",
KEYCLOAK_SAML = "keycloak-saml"
KEYCLOAK_SAML = "keycloak-saml",
AUTH0_SAML = "auth0-saml"
}
export type TCreateSamlCfgDTO = {

View File

@@ -36,7 +36,7 @@ export const sendApprovalEmailsFn = async ({
firstName: reviewerUser.firstName,
projectName: project.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
});

View File

@@ -852,7 +852,7 @@ export const secretApprovalRequestServiceFactory = ({
bypassReason,
secretPath: policy.secretPath,
environment: env.name,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`
},
template: SmtpTemplates.AccessSecretRequestBypassed
});

View File

@@ -5,6 +5,7 @@ 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 {

View File

@@ -23,6 +23,8 @@ export const KeyStorePrefixes = {
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
`identity-access-token-status:${identityAccessTokenId}`,
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
@@ -30,6 +32,7 @@ export const KeyStorePrefixes = {
export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
SetSecretSyncLastRunTimestampInSeconds: 60,
AccessTokenStatusUpdateInSeconds: 120
};

View File

@@ -1,3 +1,8 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
export const GROUPS = {
CREATE: {
name: "The name of the group to create.",
@@ -1605,3 +1610,94 @@ export const ProjectTemplates = {
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.`
})
};
export const SecretSyncs = {
LIST: (destination?: SecretSync) => ({
projectId: `The ID of the project to list ${destination ? SECRET_SYNC_NAME_MAP[destination] : "Secret"} Syncs from.`
}),
GET_BY_ID: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to retrieve.`
}),
GET_BY_NAME: (destination: SecretSync) => ({
syncName: `The name of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to retrieve.`,
projectId: `The ID of the project the ${SECRET_SYNC_NAME_MAP[destination]} Sync is associated with.`
}),
CREATE: (destination: SecretSync) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
name: `The name of the ${destinationName} Sync to create. Must be slug-friendly.`,
description: `An optional description for the ${destinationName} Sync.`,
folderId: `The ID of the project folder to sync secrets from.`,
connectionId: `The ID of the ${
APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]]
} Connection to use for syncing.`,
isEnabled: `Whether secrets should be synced automatically or not.`,
syncOptions: "Optional parameters to modify how secrets are synced."
};
},
UPDATE: (destination: SecretSync) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
syncId: `The ID of the ${destinationName} Sync to be updated.`,
name: `The updated name of the ${destinationName} Sync. Must be slug-friendly.`,
folderId: `The updated project folder ID to sync secrets from.`,
description: `The updated description of the ${destinationName} Sync.`,
isEnabled: `Whether secrets should be synced automatically or not.`,
syncOptions: "Optional parameters to modify how secrets are synced."
};
},
DELETE: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to be deleted.`
}),
SYNC: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger a sync for.`
}),
IMPORT: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger an import for.`,
shouldOverwrite: `Specify whether newly imported secrets should override existing secrets with matching names in Infisical.`
}),
ERASE: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger an erase for.`
}),
SYNC_OPTIONS: {
PREPEND_PREFIX: "Optionally prepend a prefix to your secrets' keys when syncing.",
APPEND_SUFFIX: "Optionally append a suffix to your secrets' keys when syncing."
},
DESTINATION_CONFIG: {
AWS_PARAMETER_STORE: {
REGION: "The AWS region to sync secrets to.",
PATH: "The Parameter Store path to sync secrets to."
}
}
};

View File

@@ -157,6 +157,8 @@ const envSchema = z
INFISICAL_CLOUD: zodStrBool.default("false"),
MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional()),
CAPTCHA_SITE_KEY: zpStr(z.string().optional()),
INTERCOM_ID: zpStr(z.string().optional()),
// TELEMETRY
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
@@ -180,7 +182,24 @@ const envSchema = z
HSM_SLOT: z.coerce.number().optional().default(0),
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.
.refine(

View File

@@ -14,3 +14,9 @@ export const prefixWithSlash = (str: string) => {
if (str.startsWith("/")) return str;
return `/${str}`;
};
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
export const wrapWithSlashes = (str: string) => {
return `${str.startsWith("/") ? "" : "/"}${str}${str.endsWith("/") ? "" : `/`}`;
};

View File

@@ -43,6 +43,8 @@ export type 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 {
Hard = "hard",
Soft = "soft"

View File

@@ -15,6 +15,12 @@ import {
TIntegrationSyncPayload,
TSyncSecretsDTO
} from "@app/services/secret/secret-types";
import {
TQueueSecretSyncByIdDTO,
TQueueSecretSyncEraseByIdDTO,
TQueueSecretSyncImportByIdDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO
} from "@app/services/secret-sync/secret-sync-types";
export enum QueueName {
SecretRotation = "secret-rotation",
@@ -36,7 +42,8 @@ export enum QueueName {
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
AppConnectionSecretSync = "app-connection-secret-sync"
}
export enum QueueJobs {
@@ -61,7 +68,11 @@ export enum QueueJobs {
ProjectV3Migration = "project-v3-migration",
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
ServiceTokenStatusUpdate = "service-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
AppConnectionSecretSync = "app-connection-secret-sync",
AppConnectionSecretSyncImport = "app-connection-secret-sync-import",
AppConnectionSecretSyncErase = "app-connection-secret-sync-erase",
AppConnectionSendSecretSyncActionFailedNotifications = "app-connection-send-secret-sync-action-failed-notifications"
}
export type TQueueJobTypes = {
@@ -184,6 +195,23 @@ export type TQueueJobTypes = {
};
};
};
[QueueName.AppConnectionSecretSync]:
| {
name: QueueJobs.AppConnectionSecretSync;
payload: TQueueSecretSyncByIdDTO;
}
| {
name: QueueJobs.AppConnectionSecretSyncImport;
payload: TQueueSecretSyncImportByIdDTO;
}
| {
name: QueueJobs.AppConnectionSecretSyncErase;
payload: TQueueSecretSyncEraseByIdDTO;
}
| {
name: QueueJobs.AppConnectionSendSecretSyncActionFailedNotifications;
payload: TQueueSendSecretSyncActionFailedNotificationsDTO;
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@@ -27,10 +27,10 @@ import { globalRateLimiterCfg } from "./config/rateLimiter";
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
import { apiMetrics } from "./plugins/api-metrics";
import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
import { fastifyIp } from "./plugins/ip";
import { maintenanceMode } from "./plugins/maintenanceMode";
import { registerServeUI } from "./plugins/serve-ui";
import { fastifySwagger } from "./plugins/swagger";
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 });
if (appCfg.isProductionMode) {
await server.register(registerExternalNextjs, {
standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED,
dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../"),
port: appCfg.PORT
});
}
await server.register(registerServeUI, {
standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED,
dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../")
});
await server.ready();
server.swagger();

View File

@@ -1,8 +1,10 @@
import { ForbiddenError, PureAbility } from "@casl/ability";
import opentelemetry from "@opentelemetry/api";
import fastifyPlugin from "fastify-plugin";
import jwt from "jsonwebtoken";
import { ZodError } from "zod";
import { getConfig } from "@app/lib/config/env";
import {
BadRequestError,
DatabaseError,
@@ -35,8 +37,30 @@ enum HttpStatusCodes {
}
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) => {
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) {
void res
.status(HttpStatusCodes.BadRequest)
@@ -52,13 +76,20 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
message: error.message,
error: error.name
});
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
} else if (error instanceof DatabaseError) {
void res.status(HttpStatusCodes.InternalServerError).send({
reqId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
message: "Something went wrong",
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) {
void res.status(HttpStatusCodes.GatewayTimeout).send({
reqId: req.id,

View File

@@ -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 */
}
};

View 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");
}
});
}
};

View File

@@ -91,6 +91,8 @@ import { readLimit } from "@app/server/config/rateLimiter";
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
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 { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
@@ -193,6 +195,9 @@ import { secretImportDALFactory } from "@app/services/secret-import/secret-impor
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal";
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { secretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
import { secretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-queue";
import { secretSyncServiceFactory } from "@app/services/secret-sync/secret-sync-service";
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
@@ -314,6 +319,8 @@ export const registerRoutes = async (
const auditLogStreamDAL = auditLogStreamDALFactory(db);
const trustedIpDAL = trustedIpDALFactory(db);
const telemetryDAL = telemetryDALFactory(db);
const appConnectionDAL = appConnectionDALFactory(db);
const secretSyncDAL = secretSyncDALFactory(db, folderDAL);
// ee db layer ops
const permissionDAL = permissionDALFactory(db);
@@ -818,6 +825,28 @@ export const registerRoutes = async (
kmsService
});
const secretSyncQueue = secretSyncQueueFactory({
queueService,
secretSyncDAL,
folderDAL,
secretImportDAL,
secretV2BridgeDAL,
kmsService,
keyStore,
auditLogService,
smtpService,
projectDAL,
projectMembershipDAL,
projectBotDAL,
secretDAL,
secretBlindIndexDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL
});
const secretQueueService = secretQueueFactory({
keyStore,
queueService,
@@ -851,7 +880,8 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
projectKeyDAL,
projectUserMembershipRoleDAL,
orgService
orgService,
secretSyncQueue
});
const projectService = projectServiceFactory({
@@ -1352,6 +1382,24 @@ export const registerRoutes = async (
externalGroupOrgRoleMappingDAL
});
const appConnectionService = appConnectionServiceFactory({
appConnectionDAL,
permissionService,
kmsService,
licenseService
});
const secretSyncService = secretSyncServiceFactory({
secretSyncDAL,
permissionService,
appConnectionService,
licenseService,
folderDAL,
secretSyncQueue,
projectBotService,
keyStore
});
await superAdminService.initServerCfg();
// setup the communication with license key server
@@ -1448,7 +1496,9 @@ export const registerRoutes = async (
migration: migrationService,
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
projectTemplate: projectTemplateService,
totp: totpService
totp: totpService,
appConnection: appConnectionService,
secretSync: secretSyncService
});
const cronJobs: CronJob[] = [];

View File

@@ -0,0 +1,313 @@
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,
sanitizedResponseSchema
}: {
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 }>;
sanitizedResponseSchema: 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: sanitizedResponseSchema.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: "/available",
config: {
rateLimit: readLimit
},
schema: {
description: `List the ${appName} Connections the current user has permission to establish connections with.`,
response: {
200: z.object({
appConnections: z.object({ app: z.literal(app), name: z.string(), id: z.string().uuid() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser(
app,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS,
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: sanitizedResponseSchema })
}
},
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()
.trim()
.min(1, "Connection name required")
.describe(AppConnections.GET_BY_NAME(app).connectionName)
}),
response: {
200: z.object({ appConnection: sanitizedResponseSchema })
}
},
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: sanitizedResponseSchema })
}
},
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 T;
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: sanitizedResponseSchema })
}
},
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: sanitizedResponseSchema })
}
},
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 };
}
});
};

View File

@@ -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 };
}
});
};

View File

@@ -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,
sanitizedResponseSchema: SanitizedAwsConnectionSchema,
createSchema: CreateAwsConnectionSchema,
updateSchema: UpdateAwsConnectionSchema
});

View File

@@ -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,
sanitizedResponseSchema: SanitizedGitHubConnectionSchema,
createSchema: CreateGitHubConnectionSchema,
updateSchema: UpdateGitHubConnectionSchema
});

View File

@@ -0,0 +1,12 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
export * from "./app-connection-router";
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
{
[AppConnection.AWS]: registerAwsConnectionRouter,
[AppConnection.GitHub]: registerGitHubConnectionRouter
};

View File

@@ -63,7 +63,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
schema: {
response: {
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 }
);
return { token };
return { token, organizationId: decodedToken.organizationId };
}
});
};

View File

@@ -1,5 +1,10 @@
import {
APP_CONNECTION_REGISTER_ROUTER_MAP,
registerAppConnectionRouter
} from "@app/server/routes/v1/app-connection-routers";
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers";
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
@@ -110,4 +115,30 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
await server.register(registerCmekRouter, { prefix: "/kms" });
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
await server.register(
async (appConnectionRouter) => {
// register generic app connection endpoints
await appConnectionRouter.register(registerAppConnectionRouter);
// register service specific endpoints (app-connections/aws, app-connections/github, etc.)
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_ROUTER_MAP)) {
await appConnectionRouter.register(router, { prefix: `/${app}` });
}
},
{ prefix: "/app-connections" }
);
await server.register(
async (secretSyncRouter) => {
// register generic secret sync endpoints
await secretSyncRouter.register(registerSecretSyncRouter);
// register service specific secret sync endpoints (secret-syncs/aws-parameter-store, secret-syncs/github, etc.)
for await (const [destination, router] of Object.entries(SECRET_SYNC_REGISTER_ROUTER_MAP)) {
await secretSyncRouter.register(router, { prefix: `/${destination}` });
}
},
{ prefix: "/secret-syncs" }
);
};

View File

@@ -0,0 +1,17 @@
import {
AwsParameterStoreSyncSchema,
CreateAwsParameterStoreSyncSchema,
UpdateAwsParameterStoreSyncSchema
} from "@app/services/secret-sync/aws-parameter-store";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerAwsParameterStoreSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.AWSParameterStore,
server,
responseSchema: AwsParameterStoreSyncSchema,
createSchema: CreateAwsParameterStoreSyncSchema,
updateSchema: UpdateAwsParameterStoreSyncSchema
});

View File

@@ -0,0 +1,9 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
export * from "./secret-sync-router";
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter
};

View File

@@ -0,0 +1,398 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSyncs } from "@app/lib/api-docs";
import { startsWithVowel } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretSync, TSecretSyncInput } from "@app/services/secret-sync/secret-sync-types";
export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TSecretSyncInput>({
server,
destination,
createSchema,
updateSchema,
responseSchema
}: {
destination: SecretSync;
server: FastifyZodProvider;
createSchema: z.ZodType<{
name: string;
folderId: string;
connectionId: string;
destinationConfig: I["destinationConfig"];
syncOptions?: I["syncOptions"];
description?: string | null;
}>;
updateSchema: z.ZodType<{
name?: string;
folderId?: string;
destinationConfig?: I["destinationConfig"];
syncOptions?: I["syncOptions"];
description?: string | null;
}>;
responseSchema: z.ZodTypeAny;
}) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
server.route({
method: "GET",
url: `/`,
config: {
rateLimit: readLimit
},
schema: {
description: `List the ${destinationName} Syncs for the specified project.`,
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST(destination).projectId)
}),
response: {
200: z.object({ secretSyncs: responseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId }
} = req;
const secretSyncs = (await server.services.secretSync.listSecretSyncsByProjectId(
{ projectId, destination },
req.permission
)) as T[];
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_SYNCS,
metadata: {
destination,
count: secretSyncs.length,
syncIds: secretSyncs.map((connection) => connection.id)
}
}
});
return { secretSyncs };
}
});
server.route({
method: "GET",
url: "/:syncId",
config: {
rateLimit: readLimit
},
schema: {
description: `Get the specified ${destinationName} Sync by ID.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.GET_BY_ID(destination).syncId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.findSecretSyncById(
{ syncId, destination },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretSync.projectId,
event: {
type: EventType.GET_SECRET_SYNC,
metadata: {
syncId,
destination
}
}
});
return { secretSync };
}
});
server.route({
method: "GET",
url: `/name/:syncName`,
config: {
rateLimit: readLimit
},
schema: {
description: `Get the specified ${destinationName} Sync by name and project ID.`,
params: z.object({
syncName: z.string().trim().min(1, "Sync name required").describe(SecretSyncs.GET_BY_NAME(destination).syncName)
}),
querystring: z.object({
projectId: z
.string()
.trim()
.min(1, "Project ID required")
.describe(SecretSyncs.GET_BY_NAME(destination).projectId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncName } = req.params;
const { projectId } = req.query;
const secretSync = (await server.services.secretSync.findSecretSyncByName(
{ syncName, projectId, destination },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_SYNC,
metadata: {
syncId: secretSync.id,
destination
}
}
});
return { secretSync };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: `Create ${
startsWithVowel(destinationName) ? "an" : "a"
} ${destinationName} Sync for the specified project environment.`,
body: createSchema,
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const syncOptions = req.body.syncOptions ?? {};
const secretSync = (await server.services.secretSync.createSecretSync(
{ ...req.body, destination, syncOptions },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretSync.projectId,
event: {
type: EventType.CREATE_SECRET_SYNC,
metadata: {
syncId: secretSync.id,
destination,
...req.body,
syncOptions
}
}
});
return { secretSync };
}
});
server.route({
method: "PATCH",
url: "/:syncId",
config: {
rateLimit: writeLimit
},
schema: {
description: `Update the specified ${destinationName} Connection.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).syncId)
}),
body: updateSchema,
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.updateSecretSync(
{ ...req.body, syncId, destination },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretSync.projectId,
event: {
type: EventType.UPDATE_SECRET_SYNC,
metadata: {
syncId,
destination,
...req.body
}
}
});
return { secretSync };
}
});
server.route({
method: "DELETE",
url: `/:syncId`,
config: {
rateLimit: writeLimit
},
schema: {
description: `Delete the specified ${destinationName} Connection.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.DELETE(destination).syncId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.deleteSecretSync(
{ destination, syncId },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_SECRET_SYNC,
metadata: {
destination,
syncId
}
}
});
return { secretSync };
}
});
server.route({
method: "POST",
url: "/:syncId/sync",
config: {
rateLimit: writeLimit
},
schema: {
description: `Trigger a sync for the specified ${destinationName} Sync.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.SYNC(destination).syncId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.triggerSecretSyncById(
{
syncId,
destination,
auditLogInfo: req.auditLogInfo
},
req.permission
)) as T;
return { secretSync };
}
});
server.route({
method: "POST",
url: "/:syncId/import",
config: {
rateLimit: writeLimit
},
schema: {
description: `Import secrets from the specified ${destinationName} Sync destination.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.IMPORT(destination).syncId)
}),
querystring: z.object({
shouldOverwrite: z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true")
.describe(SecretSyncs.IMPORT(destination).shouldOverwrite)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const { shouldOverwrite } = req.query;
const secretSync = (await server.services.secretSync.triggerSecretSyncImportById(
{
syncId,
destination,
shouldOverwrite
},
req.permission
)) as T;
return { secretSync };
}
});
server.route({
method: "POST",
url: "/:syncId/erase",
config: {
rateLimit: writeLimit
},
schema: {
description: `Erase synced secrets from the specified ${destinationName} Sync destination.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.ERASE(destination).syncId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.triggerSecretSyncEraseById(
{
syncId,
destination
},
req.permission
)) as T;
return { secretSync };
}
});
};

View File

@@ -0,0 +1,80 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSyncs } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
AwsParameterStoreSyncListItemSchema,
AwsParameterStoreSyncSchema
} from "@app/services/secret-sync/aws-parameter-store";
// union once more available
const SecretSyncSchema = AwsParameterStoreSyncSchema;
// union once more available
const SecretSyncOptionsSchema = AwsParameterStoreSyncListItemSchema;
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/options",
config: {
rateLimit: readLimit
},
schema: {
description: "List the available Secret Sync Options.",
response: {
200: z.object({
secretSyncOptions: SecretSyncOptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => {
const secretSyncOptions = server.services.secretSync.listSecretSyncOptions();
return { secretSyncOptions };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List all the Secret Syncs for the specified project.",
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST().projectId)
}),
response: {
200: z.object({ secretSyncs: SecretSyncSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const secretSyncs = await server.services.secretSync.listSecretSyncsByProjectId({ projectId }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_SYNCS,
metadata: {
syncIds: secretSyncs.map((sync) => sync.id),
count: secretSyncs.length
}
}
});
return { secretSyncs };
}
});
};

View File

@@ -331,12 +331,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
failureAsync: async () => {
return res.redirect(appCfg.SITE_URL as string);
},
successAsync: async (installation) => {
const metadata = JSON.parse(installation.metadata || "") as {
orgId: string;
};
return res.redirect(`${appCfg.SITE_URL}/org/${metadata.orgId}/settings?selectedTab=workflow-integrations`);
successAsync: async () => {
return res.redirect(`${appCfg.SITE_URL}/organization/settings?selectedTab=workflow-integrations`);
}
});
}

View 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 };
};

View File

@@ -0,0 +1,51 @@
export enum AppConnection {
GitHub = "github",
AWS = "aws"
}
export enum AWSRegion {
// US
US_EAST_1 = "us-east-1", // N. Virginia
US_EAST_2 = "us-east-2", // Ohio
US_WEST_1 = "us-west-1", // N. California
US_WEST_2 = "us-west-2", // Oregon
// GovCloud
US_GOV_EAST_1 = "us-gov-east-1", // US-East
US_GOV_WEST_1 = "us-gov-west-1", // US-West
// Africa
AF_SOUTH_1 = "af-south-1", // Cape Town
// Asia Pacific
AP_EAST_1 = "ap-east-1", // Hong Kong
AP_SOUTH_1 = "ap-south-1", // Mumbai
AP_SOUTH_2 = "ap-south-2", // Hyderabad
AP_NORTHEAST_1 = "ap-northeast-1", // Tokyo
AP_NORTHEAST_2 = "ap-northeast-2", // Seoul
AP_NORTHEAST_3 = "ap-northeast-3", // Osaka
AP_SOUTHEAST_1 = "ap-southeast-1", // Singapore
AP_SOUTHEAST_2 = "ap-southeast-2", // Sydney
AP_SOUTHEAST_3 = "ap-southeast-3", // Jakarta
AP_SOUTHEAST_4 = "ap-southeast-4", // Melbourne
// Canada
CA_CENTRAL_1 = "ca-central-1", // Central
// Europe
EU_CENTRAL_1 = "eu-central-1", // Frankfurt
EU_CENTRAL_2 = "eu-central-2", // Zurich
EU_WEST_1 = "eu-west-1", // Ireland
EU_WEST_2 = "eu-west-2", // London
EU_WEST_3 = "eu-west-3", // Paris
EU_SOUTH_1 = "eu-south-1", // Milan
EU_SOUTH_2 = "eu-south-2", // Spain
EU_NORTH_1 = "eu-north-1", // Stockholm
// Middle East
ME_SOUTH_1 = "me-south-1", // Bahrain
ME_CENTRAL_1 = "me-central-1", // UAE
// South America
SA_EAST_1 = "sa-east-1" // Sao Paulo
}

View File

@@ -0,0 +1,106 @@
import { TAppConnections } from "@app/db/schemas/app-connections";
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}`);
}
};
export const decryptAppConnection = async (
appConnection: TAppConnections,
kmsService: TAppConnectionServiceFactoryDep["kmsService"]
) => {
return {
...appConnection,
credentials: await decryptAppConnectionCredentials({
encryptedCredentials: appConnection.encryptedCredentials,
orgId: appConnection.orgId,
kmsService
})
} as TAppConnection;
};

View File

@@ -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"
};

View File

@@ -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)
});

View File

@@ -0,0 +1,396 @@
import { ForbiddenError, subject } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
decryptAppConnection,
encryptAppConnectionCredentials,
getAppConnectionMethodName,
listAppConnectionOptions,
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(
OrgPermissionAppConnectionActions.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((appConnection) => decryptAppConnection(appConnection, kmsService))
);
};
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(
OrgPermissionAppConnectionActions.Read,
OrgPermissionSubjects.AppConnections
);
if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
return decryptAppConnection(appConnection, kmsService);
};
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(
OrgPermissionAppConnectionActions.Read,
OrgPermissionSubjects.AppConnections
);
if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
return decryptAppConnection(appConnection, kmsService);
};
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(
OrgPermissionAppConnectionActions.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 as TAppConnection;
};
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(
OrgPermissionAppConnectionActions.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 decryptAppConnection(updatedAppConnection, kmsService);
};
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(
OrgPermissionAppConnectionActions.Delete,
OrgPermissionSubjects.AppConnections
);
if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
// TODO (scott): add option to delete all dependencies
try {
const deletedAppConnection = await appConnectionDAL.deleteById(connectionId);
return await decryptAppConnection(deletedAppConnection, kmsService);
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === "23503") {
throw new BadRequestError({
message:
"Cannot delete App Connection with existing connections. Remove all existing connections and try again."
});
}
throw err;
}
};
const connectAppConnectionById = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
appConnection.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionAppConnectionActions.Connect,
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id })
);
return decryptAppConnection(appConnection, kmsService);
};
const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor) => {
await checkAppServicesAvailability(actor.orgId);
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
const appConnections = await appConnectionDAL.find({ app, orgId: actor.orgId });
const availableConnections = appConnections.filter((connection) =>
orgPermission.can(
OrgPermissionAppConnectionActions.Connect,
subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id })
)
);
return availableConnections as Omit<TAppConnection, "credentials">[];
};
return {
listAppConnectionOptions,
listAppConnectionsByOrg,
findAppConnectionById,
findAppConnectionByName,
createAppConnection,
updateAppConnection,
deleteAppConnection,
connectAppConnectionById,
listAvailableAppConnectionsForUser
};
};

View 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;

View File

@@ -0,0 +1,4 @@
export enum AwsConnectionMethod {
AssumeRole = "assume-role",
AccessKey = "access-key"
}

View File

@@ -0,0 +1,105 @@
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import AWS from "aws-sdk";
import { randomUUID } from "crypto";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { AwsConnectionMethod } from "./aws-connection-enums";
import { TAwsConnectionConfig } from "./aws-connection-types";
export const getAwsAppConnectionListItem = () => {
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
return {
name: "AWS" as const,
app: AppConnection.AWS as const,
methods: Object.values(AwsConnectionMethod) as [AwsConnectionMethod.AssumeRole, AwsConnectionMethod.AccessKey],
accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
};
};
export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig, region = AWSRegion.US_EAST_1) => {
const appCfg = getConfig();
let accessKeyId: string;
let secretAccessKey: string;
let sessionToken: undefined | string;
const { method, credentials, orgId } = appConnection;
switch (method) {
case AwsConnectionMethod.AssumeRole: {
const client = new STSClient({
region,
credentials:
appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID && appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
? {
accessKeyId: appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID,
secretAccessKey: appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
}
: undefined // if hosting on AWS
});
const command = new AssumeRoleCommand({
RoleArn: credentials.roleArn,
RoleSessionName: `infisical-app-connection-${randomUUID()}`,
DurationSeconds: 900, // 15 mins
ExternalId: orgId
});
const assumeRes = await client.send(command);
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
}
accessKeyId = assumeRes.Credentials.AccessKeyId;
secretAccessKey = assumeRes.Credentials.SecretAccessKey;
sessionToken = assumeRes.Credentials?.SessionToken;
break;
}
case AwsConnectionMethod.AccessKey: {
accessKeyId = credentials.accessKeyId;
secretAccessKey = credentials.secretAccessKey;
break;
}
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new InternalServerError({ message: `Unsupported AWS connection method: ${method}` });
}
return new AWS.Config({
region,
credentials: {
accessKeyId,
secretAccessKey,
sessionToken
}
});
};
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
const awsConfig = await getAwsConnectionConfig(appConnection);
const sts = new AWS.STS(awsConfig);
let resp: Awaited<ReturnType<ReturnType<typeof sts.getCallerIdentity>["promise"]>>;
try {
resp = await sts.getCallerIdentity().promise();
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection - verify credentials`
});
}
if (resp.$response.httpResponse.statusCode !== 200)
throw new InternalServerError({
message: `Unable to validate credentials: ${
resp.$response.error?.message ??
`AWS responded with a status code of ${resp.$response.httpResponse.statusCode}. Verify credentials and try again.`
}`
});
return appConnection.credentials;
};

View File

@@ -0,0 +1,82 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { AwsConnectionMethod } from "./aws-connection-enums";
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
roleArn: z.string().trim().min(1, "Role ARN required")
});
export const AwsConnectionAccessTokenCredentialsSchema = z.object({
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
secretAccessKey: z.string().trim().min(1, "Secret Access Key required")
});
const BaseAwsConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AWS) });
export const AwsConnectionSchema = z.intersection(
BaseAwsConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(AwsConnectionMethod.AssumeRole),
credentials: AwsConnectionAssumeRoleCredentialsSchema
}),
z.object({
method: z.literal(AwsConnectionMethod.AccessKey),
credentials: AwsConnectionAccessTokenCredentialsSchema
})
])
);
export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
BaseAwsConnectionSchema.extend({
method: z.literal(AwsConnectionMethod.AssumeRole),
credentials: AwsConnectionAssumeRoleCredentialsSchema.omit({ roleArn: true })
}),
BaseAwsConnectionSchema.extend({
method: z.literal(AwsConnectionMethod.AccessKey),
credentials: AwsConnectionAccessTokenCredentialsSchema.omit({ secretAccessKey: true })
})
]);
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections?.CREATE(AppConnection.AWS).method),
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(AppConnections.CREATE(AppConnection.AWS).credentials)
}),
z.object({
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections?.CREATE(AppConnection.AWS).method),
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AWS).credentials
)
})
]);
export const CreateAwsConnectionSchema = ValidateAwsConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.AWS)
);
export const UpdateAwsConnectionSchema = z
.object({
credentials: z
.union([AwsConnectionAccessTokenCredentialsSchema, AwsConnectionAssumeRoleCredentialsSchema])
.optional()
.describe(AppConnections.UPDATE(AppConnection.AWS).credentials)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS));
export const AwsConnectionListItemSchema = z.object({
name: z.literal("AWS"),
app: z.literal(AppConnection.AWS),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(AwsConnectionMethod).array(),
accessKeyId: z.string().optional()
});

View File

@@ -0,0 +1,22 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
AwsConnectionSchema,
CreateAwsConnectionSchema,
ValidateAwsConnectionCredentialsSchema
} from "./aws-connection-schemas";
export type TAwsConnection = z.infer<typeof AwsConnectionSchema>;
export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
app: AppConnection.AWS;
};
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema;
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};

View File

@@ -0,0 +1,4 @@
export * from "./aws-connection-enums";
export * from "./aws-connection-fns";
export * from "./aws-connection-schemas";
export * from "./aws-connection-types";

View File

@@ -0,0 +1,4 @@
export enum GitHubConnectionMethod {
OAuth = "oauth",
App = "github-app"
}

View File

@@ -0,0 +1,129 @@
import { AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { GitHubConnectionMethod } from "./github-connection-enums";
import { TGitHubConnectionConfig } from "./github-connection-types";
export const getGitHubConnectionListItem = () => {
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
return {
name: "GitHub" as const,
app: AppConnection.GitHub as const,
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
};
};
type TokenRespData = {
access_token: string;
scope: string;
token_type: string;
};
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config;
const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET,
SITE_URL
} = getConfig();
const { clientId, clientSecret } =
method === GitHubConnectionMethod.App
? {
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
}
: // oauth
{
clientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
clientSecret: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET
};
if (!clientId || !clientSecret) {
throw new InternalServerError({
message: `GitHub ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
}
let tokenResp: AxiosResponse<TokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: clientId,
client_secret: clientSecret,
code: credentials.code,
redirect_uri: `${SITE_URL}/app-connections/github/oauth/callback`
},
headers: {
Accept: "application/json",
"Accept-Encoding": "application/json"
}
});
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection - verify credentials`
});
}
if (tokenResp.status !== 200) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
});
}
if (method === GitHubConnectionMethod.App) {
const installationsResp = await request.get<{
installations: {
id: number;
account: {
login: string;
};
}[];
}>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${tokenResp.data.access_token}`,
"Accept-Encoding": "application/json"
}
});
const matchingInstallation = installationsResp.data.installations.find(
(installation) => installation.id === +credentials.installationId
);
if (!matchingInstallation) {
throw new ForbiddenRequestError({
message: "User does not have access to the provided installation"
});
}
}
switch (method) {
case GitHubConnectionMethod.App:
return {
// access token not needed for GitHub App
installationId: credentials.installationId
};
case GitHubConnectionMethod.OAuth:
return {
accessToken: tokenResp.data.access_token
};
default:
throw new InternalServerError({
message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}`
});
}
};

View File

@@ -0,0 +1,93 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { GitHubConnectionMethod } from "./github-connection-enums";
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "OAuth code required")
});
export const GitHubConnectionAppInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "GitHub App code required"),
installationId: z.string().min(1, "GitHub App Installation ID required")
});
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
accessToken: z.string()
});
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
installationId: z.string()
});
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
credentials: GitHubConnectionAppInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GitHub).credentials
)
}),
z.object({
method: z.literal(GitHubConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitHub).method),
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GitHub).credentials
)
})
]);
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub)
);
export const UpdateGitHubConnectionSchema = z
.object({
credentials: z
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
.optional()
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub));
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
export const GitHubConnectionSchema = z.intersection(
BaseGitHubConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(GitHubConnectionMethod.App),
credentials: GitHubConnectionAppOutputCredentialsSchema
}),
z.object({
method: z.literal(GitHubConnectionMethod.OAuth),
credentials: GitHubConnectionOAuthOutputCredentialsSchema
})
])
);
export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.App),
credentials: GitHubConnectionAppOutputCredentialsSchema.omit({ installationId: true })
}),
BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.OAuth),
credentials: GitHubConnectionOAuthOutputCredentialsSchema.omit({ accessToken: true })
})
]);
export const GitHubConnectionListItemSchema = z.object({
name: z.literal("GitHub"),
app: z.literal(AppConnection.GitHub),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GitHubConnectionMethod).array(),
oauthClientId: z.string().optional(),
appClientSlug: z.string().optional()
});

View File

@@ -0,0 +1,20 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateGitHubConnectionSchema,
GitHubConnectionSchema,
ValidateGitHubConnectionCredentialsSchema
} from "./github-connection-schemas";
export type TGitHubConnection = z.infer<typeof GitHubConnectionSchema>;
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
app: AppConnection.GitHub;
};
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;

View File

@@ -0,0 +1,4 @@
export * from "./github-connection-enums";
export * from "./github-connection-fns";
export * from "./github-connection-schemas";
export * from "./github-connection-types";

View File

@@ -305,10 +305,16 @@ const syncSecretsAzureAppConfig = async ({
value: string;
}
const getCompleteAzureAppConfigValues = async (url: string) => {
if (!integration.app || !integration.app.endsWith(".azconfig.io"))
throw new BadRequestError({
message: "Invalid Azure App Configuration URL provided."
});
const getCompleteAzureAppConfigValues = async (baseURL: string, url: string) => {
let result: AzureAppConfigKeyValue[] = [];
while (url) {
const res = await request.get(url, {
baseURL,
headers: {
Authorization: `Bearer ${accessToken}`
},
@@ -319,7 +325,7 @@ const syncSecretsAzureAppConfig = async ({
});
result = result.concat(res.data.items);
url = res.data.nextLink;
url = res.data?.["@nextLink"];
}
return result;
@@ -327,11 +333,13 @@ const syncSecretsAzureAppConfig = async ({
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
const azureAppConfigValuesUrl = `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
metadata.azureLabel ? `&label=${metadata.azureLabel}` : ""
const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
metadata.azureLabel ? `&label=${metadata.azureLabel}` : "&label=%00"
}`;
const azureAppConfigSecrets = (await getCompleteAzureAppConfigValues(azureAppConfigValuesUrl)).reduce(
const azureAppConfigSecrets = (
await getCompleteAzureAppConfigValues(integration.app, azureAppConfigValuesUrl)
).reduce(
(accum, entry) => {
accum[entry.key] = entry.value;
@@ -1159,7 +1167,8 @@ const syncSecretsAWSSecretManager = async ({
} else {
await secretsManager.send(
new DeleteSecretCommand({
SecretId: secretId
SecretId: secretId,
ForceDeleteWithoutRecovery: true
})
);
}
@@ -1397,14 +1406,24 @@ const syncSecretsHeroku = async ({
* Sync/push [secrets] to Vercel project named [integration.app]
*/
const syncSecretsVercel = async ({
createManySecretsRawFn,
integration,
integrationAuth,
secrets,
secrets: infisicalSecrets,
accessToken
}: {
integration: TIntegrations;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string;
}) => {
interface VercelSecret {
@@ -1477,80 +1496,119 @@ const syncSecretsVercel = async ({
}
}
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
// Identify secrets to create
Object.keys(secrets).forEach((key) => {
if (!(key in res)) {
// case: secret has been created
newSecrets.push({
key,
value: secrets[key].value,
type: "encrypted",
target: [integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
});
// Default to overwrite target for old integrations that doesn't have a initial sync behavior set.
if (!metadata.initialSyncBehavior) {
metadata.initialSyncBehavior = IntegrationInitialSyncBehavior.OVERWRITE_TARGET;
}
const secretsToAddToInfisical: { [key: string]: VercelSecret } = {};
Object.keys(res).forEach((vercelKey) => {
if (!integration.lastUsed) {
// first time using integration
// -> apply initial sync behavior
switch (metadata.initialSyncBehavior) {
// Override all the secrets in Vercel
case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
if (!(vercelKey in infisicalSecrets)) infisicalSecrets[vercelKey] = null;
break;
}
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
// if the vercel secret is not in infisical, we need to add it to infisical
if (!(vercelKey in infisicalSecrets)) {
infisicalSecrets[vercelKey] = {
value: res[vercelKey].value
};
secretsToAddToInfisical[vercelKey] = res[vercelKey];
}
break;
}
default: {
throw new Error(`Invalid initial sync behavior: ${metadata.initialSyncBehavior}`);
}
}
} else if (!(vercelKey in infisicalSecrets)) {
infisicalSecrets[vercelKey] = null;
}
});
// Identify secrets to update and delete
Object.keys(res).forEach((key) => {
if (key in secrets) {
if (res[key].value !== secrets[key].value) {
// case: secret value has changed
updateSecrets.push({
id: res[key].id,
key,
value: secrets[key].value,
type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment as string)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
id: res[key].id,
key,
value: res[key].value,
type: "encrypted", // value doesn't matter
target: [integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
});
}
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await request.post(`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`, newSecrets, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
if (Object.keys(secretsToAddToInfisical).length) {
await createManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToAddToInfisical).map((key) => ({
secretName: key,
secretValue: secretsToAddToInfisical[key].value,
type: SecretType.Shared,
secretComment: ""
}))
});
}
for await (const secret of updateSecrets) {
if (secret.type !== "sensitive") {
const { id, ...updatedSecret } = secret;
await request.patch(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${id}`, updatedSecret, {
// update and create logic
for await (const key of Object.keys(infisicalSecrets)) {
if (!(key in res) || infisicalSecrets[key]?.value !== res[key].value) {
// if the key is not in the vercel res, we need to create it
if (!(key in res)) {
await request.post(
`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`,
{
key,
value: infisicalSecrets[key]?.value,
type: "encrypted",
target: [integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
},
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
// Else if the key already exists and its not sensitive, we need to update it
} else if (res[key].type !== "sensitive") {
await request.patch(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`,
{
key,
value: infisicalSecrets[key]?.value,
type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment as string)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
},
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
}
}
// delete logic
for await (const key of Object.keys(res)) {
if (infisicalSecrets[key] === null) {
// case: delete secret
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -1559,16 +1617,6 @@ const syncSecretsVercel = async ({
});
}
}
for await (const secret of deleteSecrets) {
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
}
};
/**
@@ -4471,7 +4519,8 @@ export const syncIntegrationSecrets = async ({
integration,
integrationAuth,
secrets,
accessToken
accessToken,
createManySecretsRawFn
});
break;
case Integrations.NETLIFY:

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const AWS_PARAMETER_STORE_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "AWS Parameter Store",
destination: SecretSync.AWSParameterStore,
connection: AppConnection.AWS,
supportsImport: true
};

View File

@@ -0,0 +1,198 @@
import AWS, { AWSError } from "aws-sdk";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { TSecretMap, TSecretSyncWithConnection } from "@app/services/secret-sync/secret-sync-types";
import { TAwsParameterStoreSyncWithConnection } from "./aws-parameter-store-sync-types";
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
const MAX_RETRIES = 5;
const BATCH_SIZE = 10;
const getSSM = async (secretSync: TSecretSyncWithConnection) => {
const { destinationConfig, connection } = secretSync;
const config = await getAwsConnectionConfig(connection, destinationConfig.region);
const ssm = new AWS.SSM({
apiVersion: "2014-11-06",
region: destinationConfig.region
});
ssm.config.update(config);
return ssm;
};
const sleep = async () =>
new Promise((resolve) => {
setTimeout(resolve, 1000);
});
const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreRecord> => {
const awsParameterStoreSecretsRecord: TAWSParameterStoreRecord = {};
let hasNext = true;
let nextToken: string | undefined;
let attempt = 0;
while (hasNext) {
try {
// eslint-disable-next-line no-await-in-loop
const parameters = await ssm
.getParametersByPath({
Path: path,
Recursive: false,
WithDecryption: true,
MaxResults: BATCH_SIZE,
NextToken: nextToken
})
.promise();
attempt = 0;
if (parameters.Parameters) {
parameters.Parameters.forEach((parameter) => {
if (parameter.Name) {
const secKey = parameter.Name.substring(path.length);
awsParameterStoreSecretsRecord[secKey] = parameter;
}
});
}
hasNext = Boolean(parameters.NextToken);
nextToken = parameters.NextToken;
} catch (e) {
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
attempt += 1;
// eslint-disable-next-line no-await-in-loop
await sleep();
}
throw e;
}
}
return awsParameterStoreSecretsRecord;
};
const putParameter = async (
ssm: AWS.SSM,
params: AWS.SSM.PutParameterRequest,
attempt = 0
): Promise<AWS.SSM.PutParameterResult> => {
try {
return await ssm.putParameter(params).promise();
} catch (error) {
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
await sleep();
// retry
return putParameter(ssm, params, attempt + 1);
}
throw error;
}
};
const deleteParametersBatch = async (
ssm: AWS.SSM,
parameters: AWS.SSM.Parameter[],
attempt = 0
): Promise<AWS.SSM.DeleteParameterResult[]> => {
const results: AWS.SSM.DeleteParameterResult[] = [];
let remainingParams = [...parameters];
while (remainingParams.length > 0) {
const batch = remainingParams.slice(0, BATCH_SIZE);
try {
// eslint-disable-next-line no-await-in-loop
const result = await ssm.deleteParameters({ Names: batch.map((param) => param.Name!) }).promise();
results.push(result);
remainingParams = remainingParams.slice(BATCH_SIZE);
} catch (error) {
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
// eslint-disable-next-line no-await-in-loop
await sleep();
// Retry the current batch
// eslint-disable-next-line no-await-in-loop
return [...results, ...(await deleteParametersBatch(ssm, remainingParams, attempt + 1))];
}
throw error;
}
}
return results;
};
export const AwsParameterStoreSyncFns = {
sync: async (secretSync: TAwsParameterStoreSyncWithConnection, secrets: TSecretMap) => {
const { destinationConfig } = secretSync;
const ssm = await getSSM(secretSync);
// TODO(scott): KMS Key ID, Tags
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
for await (const entry of Object.entries(secrets)) {
const [key, { value }] = entry;
// skip empty values (not allowed by AWS) or secrets that haven't changed
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) {
// eslint-disable-next-line no-continue
continue;
}
await putParameter(ssm, {
Name: `${destinationConfig.path}${key}`,
Type: "SecureString",
Value: value,
Overwrite: true
});
}
const parametersToDelete: AWS.SSM.Parameter[] = [];
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
const [key, parameter] = entry;
if (!(key in secrets) || !secrets[key].value) {
parametersToDelete.push(parameter);
}
}
await deleteParametersBatch(ssm, parametersToDelete);
},
import: async (secretSync: TAwsParameterStoreSyncWithConnection): Promise<TSecretMap> => {
const { destinationConfig } = secretSync;
const ssm = await getSSM(secretSync);
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
return Object.fromEntries(
Object.entries(awsParameterStoreSecretsRecord).map(([key, value]) => [key, { value: value.Value ?? "" }])
);
},
erase: async (secretSync: TAwsParameterStoreSyncWithConnection, secrets: TSecretMap) => {
const { destinationConfig } = secretSync;
const ssm = await getSSM(secretSync);
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
const parametersToDelete: AWS.SSM.Parameter[] = [];
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
const [key, param] = entry;
if (key in secrets) {
parametersToDelete.push(param);
}
}
await deleteParametersBatch(ssm, parametersToDelete);
}
};

View File

@@ -0,0 +1,59 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { wrapWithSlashes } from "@app/lib/fn";
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.REGION),
path: z
.string()
.min(1, "Parameter Store Path Required")
.transform(wrapWithSlashes)
.superRefine((val, ctx) => {
if (!/^\/([\w-]+\/)*[\w-]+\/$/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid Parameter Store Path - must follow "/example/path/" format`
});
}
if (val.length > 2048) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid Parameter Store Path - cannot exceed 2048 characters`
});
}
})
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.PATH)
});
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(AppConnection.AWS).extend({
destination: z.literal(SecretSync.AWSParameterStore),
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
});
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.AWSParameterStore
).extend({
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
});
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.AWSParameterStore
).extend({
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
});
export const AwsParameterStoreSyncListItemSchema = z.object({
name: z.literal("AWS Parameter Store"),
connection: z.literal(AppConnection.AWS),
destination: z.literal(SecretSync.AWSParameterStore),
supportsImport: z.literal(true)
});

View File

@@ -0,0 +1,19 @@
import { z } from "zod";
import { TAwsConnection } from "@app/services/app-connection/aws";
import {
AwsParameterStoreSyncListItemSchema,
AwsParameterStoreSyncSchema,
CreateAwsParameterStoreSyncSchema
} from "./aws-parameter-store-sync-schemas";
export type TAwsParameterStoreSync = z.infer<typeof AwsParameterStoreSyncSchema>;
export type TAwsParameterStoreSyncInput = z.infer<typeof CreateAwsParameterStoreSyncSchema>;
export type TAwsParameterStoreSyncListItem = z.infer<typeof AwsParameterStoreSyncListItemSchema>;
export type TAwsParameterStoreSyncWithConnection = Omit<TAwsParameterStoreSync, "connection"> & {
connection: TAwsConnection;
};

View File

@@ -0,0 +1,4 @@
export * from "./aws-parameter-store-sync-constants";
export * from "./aws-parameter-store-sync-fns";
export * from "./aws-parameter-store-sync-schemas";
export * from "./aws-parameter-store-sync-types";

View File

@@ -0,0 +1,201 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TSecretSyncs } from "@app/db/schemas/secret-syncs";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
export type TSecretSyncDALFactory = ReturnType<typeof secretSyncDALFactory>;
type SecretSyncFindFilter = Parameters<typeof buildFindFilter<TSecretSyncs>>[0];
const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: SecretSyncFindFilter; tx?: Knex }) => {
const query = (tx || db.replicaNode())(TableName.SecretSync)
.join(TableName.SecretFolder, `${TableName.SecretSync}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(TableName.AppConnection, `${TableName.SecretSync}.connectionId`, `${TableName.AppConnection}.id`)
.select(selectAllTableCols(TableName.SecretSync))
.select(
// evironment
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("projectId").withSchema(TableName.Environment),
// entire connection
db.ref("name").withSchema(TableName.AppConnection).as("connectionName"),
db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"),
db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"),
db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"),
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt")
);
// prepends table name to filter keys to avoid ambiguous col references, skipping utility filters like $in, etc.
const prependTableName = (filterObj: object): SecretSyncFindFilter =>
Object.fromEntries(
Object.entries(filterObj).map(([key, value]) =>
key.startsWith("$") ? [key, prependTableName(value as object)] : [`${TableName.SecretSync}.${key}`, value]
)
);
if (filter) {
/* eslint-disable @typescript-eslint/no-misused-promises */
void query.where(buildFindFilter(prependTableName(filter)));
}
return query;
};
const expandSecretSync = (
secretSync: Awaited<ReturnType<typeof baseSecretSyncQuery>>[number],
folder: Awaited<ReturnType<TSecretFolderDALFactory["findSecretPathByFolderIds"]>>[number]
) => {
const {
envId,
envName,
envSlug,
connectionApp,
connectionName,
connectionId,
connectionOrgId,
connectionEncryptedCredentials,
connectionMethod,
connectionDescription,
connectionCreatedAt,
connectionUpdatedAt,
connectionVersion,
...el
} = secretSync;
return {
...el,
connectionId,
environment: { id: envId, name: envName, slug: envSlug },
connection: {
app: connectionApp,
id: connectionId,
name: connectionName,
orgId: connectionOrgId,
encryptedCredentials: connectionEncryptedCredentials,
method: connectionMethod,
description: connectionDescription,
createdAt: connectionCreatedAt,
updatedAt: connectionUpdatedAt,
version: connectionVersion
},
folder: {
id: folder!.id,
path: folder!.path
}
};
};
export const secretSyncDALFactory = (
db: TDbClient,
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">
) => {
const secretSyncOrm = ormify(db, TableName.SecretSync);
const findById = async (id: string, tx?: Knex) => {
try {
const secretSync = await baseSecretSyncQuery({
filter: { id },
db,
tx
}).first();
if (secretSync) {
// TODO (scott): replace with cached folder path once implemented
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]);
return expandSecretSync(secretSync, folderWithPath);
}
} catch (error) {
throw new DatabaseError({ error, name: "Find by ID - Secret Sync" });
}
};
const create = async (data: Parameters<(typeof secretSyncOrm)["create"]>[0]) => {
try {
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
const sync = await secretSyncOrm.create(data, tx);
return baseSecretSyncQuery({
filter: { id: sync.id },
db,
tx
}).first();
}))!;
// TODO (scott): replace with cached folder path once implemented
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]);
return expandSecretSync(secretSync, folderWithPath);
} catch (error) {
throw new DatabaseError({ error, name: "Create - Secret Sync" });
}
};
const updateById = async (syncId: string, data: Parameters<(typeof secretSyncOrm)["updateById"]>[1]) => {
try {
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
const sync = await secretSyncOrm.updateById(syncId, data, tx);
return baseSecretSyncQuery({
filter: { id: sync.id },
db,
tx
}).first();
}))!;
// TODO (scott): replace with cached folder path once implemented
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]);
return expandSecretSync(secretSync, folderWithPath);
} catch (error) {
throw new DatabaseError({ error, name: "Update by ID - Secret Sync" });
}
};
const findOne = async (filter: Parameters<(typeof secretSyncOrm)["findOne"]>[0], tx?: Knex) => {
try {
const secretSync = await baseSecretSyncQuery({ filter, db, tx }).first();
if (secretSync) {
// TODO (scott): replace with cached folder path once implemented
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]);
return expandSecretSync(secretSync, folderWithPath);
}
} catch (error) {
throw new DatabaseError({ error, name: "Find One - Secret Sync" });
}
};
const find = async (filter: Parameters<(typeof secretSyncOrm)["find"]>[0], tx?: Knex) => {
try {
const secretSyncs = await baseSecretSyncQuery({ filter, db, tx });
if (!secretSyncs.length) return [];
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
secretSyncs[0].projectId,
secretSyncs.map((sync) => sync.folderId)
);
// TODO (scott): replace with cached folder path once implemented
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
foldersWithPath.forEach((folder) => {
if (folder) folderRecord[folder.id] = folder;
});
return secretSyncs.map((secretSync) => expandSecretSync(secretSync, folderRecord[secretSync.folderId]));
} catch (error) {
throw new DatabaseError({ error, name: "Find - Secret Sync" });
}
};
return { ...secretSyncOrm, findById, findOne, find, create, updateById };
};

View File

@@ -0,0 +1,3 @@
export enum SecretSync {
AWSParameterStore = "aws-parameter-store"
}

View File

@@ -0,0 +1,70 @@
import { BadRequestError } from "@app/lib/errors";
import {
AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
AwsParameterStoreSyncFns
} from "@app/services/secret-sync/aws-parameter-store";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import {
TSecretMap,
TSecretSyncListItem,
TSecretSyncWithConnection
} from "@app/services/secret-sync/secret-sync-types";
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
};
const processSyncOptions = (secretSync: TSecretSyncWithConnection, unprocessedSecretMap: TSecretMap) => {
let secretMap = { ...unprocessedSecretMap };
const { appendSuffix, prependPrefix } = secretSync.syncOptions;
if (appendSuffix || prependPrefix) {
secretMap = {};
Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
});
}
return secretMap;
};
export const SecretSyncFns = {
sync: (secretSync: TSecretSyncWithConnection, unprocessedSecretMap: TSecretMap): Promise<void> => {
const secretMap = processSyncOptions(secretSync, unprocessedSecretMap);
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.sync(secretSync, secretMap);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled sync destination for push secrets: ${secretSync.destination}`);
}
},
import: (secretSync: TSecretSyncWithConnection): Promise<TSecretMap> => {
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.import(secretSync);
default:
throw new BadRequestError({
message: `${SECRET_SYNC_NAME_MAP[secretSync.destination as SecretSync]} Syncs do not support pulling.`
});
}
},
erase: (secretSync: TSecretSyncWithConnection, unprocessedSecretMap: TSecretMap): Promise<void> => {
const secretMap = processSyncOptions(secretSync, unprocessedSecretMap);
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.erase(secretSync, secretMap);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled sync destination for purging secrets: ${secretSync.destination}`);
}
}
};

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.AWSParameterStore]: "AWS Parameter Store"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AWSParameterStore]: AppConnection.AWS
};

View File

@@ -0,0 +1,821 @@
import opentelemetry from "@opentelemetry/api";
import { AxiosError } from "axios";
import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { InternalServerError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "@app/services/secret-import/secret-import-fns";
import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import {
SecretSyncAction,
SecretSyncStatus,
TQueueSecretSyncByIdDTO,
TQueueSecretSyncEraseByIdDTO,
TQueueSecretSyncImportByIdDTO,
TQueueSecretSyncsByPathDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO,
TSecretMap,
TSecretSyncDTO,
TSecretSyncEraseDTO,
TSecretSyncImportDTO,
TSecretSyncRaw,
TSecretSyncWithConnection,
TSendSecretSyncFailedNotificationsJobDTO
} from "@app/services/secret-sync/secret-sync-types";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { expandSecretReferencesFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
type TSecretSyncQueueFactoryDep = {
queueService: Pick<TQueueServiceFactory, "queue" | "start">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
folderDAL: TSecretFolderDALFactory;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
| "findByFolderId"
| "find"
| "insertMany"
| "upsertSecretReferences"
| "findBySecretKeys"
| "bulkUpdate"
| "deleteMany"
>;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
projectDAL: TProjectDALFactory;
smtpService: Pick<TSmtpService, "sendMail">;
projectBotDAL: TProjectBotDALFactory;
secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionTagDALFactory;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
};
export const secretSyncQueueFactory = ({
queueService,
kmsService,
keyStore,
folderDAL,
secretV2BridgeDAL,
secretImportDAL,
secretSyncDAL,
auditLogService,
projectMembershipDAL,
projectDAL,
smtpService,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL
}: TSecretSyncQueueFactoryDep) => {
const appCfg = getConfig();
const integrationMeter = opentelemetry.metrics.getMeter("SecretSyncs");
const syncErrorHistogram = integrationMeter.createHistogram("secret_sync_errors", {
description: "Secret Sync - sync errors",
unit: "1"
});
const importErrorHistogram = integrationMeter.createHistogram("secret_sync_import_errors", {
description: "Secret Sync - import errors",
unit: "1"
});
const eraseErrorHistogram = integrationMeter.createHistogram("secret_sync_erase_errors", {
description: "Secret Sync - erase errors",
unit: "1"
});
const $createManySecretsRawFn = createManySecretsRawFnFactory({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
kmsService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL
});
const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
kmsService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL
});
const $getSecrets = async (secretSync: TSecretSyncRaw, includeImports = true) => {
const {
projectId,
folderId,
environment: { slug: environmentSlug },
folder: { path: secretPath }
} = secretSync;
const secretMap: TSecretMap = {};
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptSecretValue = (value?: Buffer | undefined | null) =>
value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "";
const { expandSecretReferences } = expandSecretReferencesFactory({
decryptSecretValue,
secretDAL: secretV2BridgeDAL,
folderDAL,
projectId,
canExpandValue: () => true
});
const secrets = await secretV2BridgeDAL.findByFolderId(folderId);
await Promise.allSettled(
secrets.map(async (secret) => {
const secretKey = secret.key;
const secretValue = decryptSecretValue(secret.encryptedValue);
const expandedSecretValue = await expandSecretReferences({
environment: environmentSlug,
secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
});
secretMap[secretKey] = { value: expandedSecretValue || "" };
if (secret.encryptedComment) {
const commentValue = decryptSecretValue(secret.encryptedComment);
secretMap[secretKey].comment = commentValue;
}
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
})
);
if (!includeImports) return secretMap;
const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
if (secretImports.length) {
const importedSecrets = await fnSecretsV2FromImports({
decryptor: decryptSecretValue,
folderDAL,
secretDAL: secretV2BridgeDAL,
expandSecretReferences,
secretImportDAL,
secretImports,
hasSecretAccess: () => true
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j];
if (!secretMap[importedSecret.key]) {
secretMap[importedSecret.key] = {
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
comment: importedSecret.secretComment,
value: importedSecret.secretValue || ""
};
}
}
}
}
return secretMap;
};
const queueSecretSyncById = async (payload: TQueueSecretSyncByIdDTO) =>
queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.AppConnectionSecretSync, payload, {
attempts: 5,
delay: 1000,
backoff: {
type: "exponential",
delay: 3000
},
removeOnComplete: true,
removeOnFail: true
});
const queueSecretSyncImportById = async (payload: TQueueSecretSyncImportByIdDTO) =>
queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.AppConnectionSecretSyncImport, payload, {
attempts: 5,
delay: 1000,
backoff: {
type: "exponential",
delay: 3000
},
removeOnComplete: true,
removeOnFail: true
});
const queueSecretSyncEraseById = async (payload: TQueueSecretSyncEraseByIdDTO) =>
queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.AppConnectionSecretSyncErase, payload, {
attempts: 5,
delay: 1000,
backoff: {
type: "exponential",
delay: 3000
},
removeOnComplete: true,
removeOnFail: true
});
const $queueSendSecretSyncFailedNotifications = async (payload: TQueueSendSecretSyncActionFailedNotificationsDTO) => {
if (!appCfg.isSmtpConfigured) return;
await queueService.queue(
QueueName.AppConnectionSecretSync,
QueueJobs.AppConnectionSendSecretSyncActionFailedNotifications,
payload,
{
jobId: `secret-sync-${payload.secretSync.id}-failed-notifications`,
attempts: 5,
delay: 1000 * 60,
backoff: {
type: "exponential",
delay: 3000
},
removeOnFail: true,
removeOnComplete: true
}
);
};
const $syncSecrets = async (job: TSecretSyncDTO) => {
const {
data: { syncId, auditLogInfo }
} = job;
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
await secretSyncDAL.updateById(syncId, {
syncStatus: SecretSyncStatus.Pending
});
logger.info(
`SecretSync Sync [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
);
let isSynced = false;
let syncMessage: string | null = null;
const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
try {
const {
connection: { orgId, encryptedCredentials }
} = secretSync;
const credentials = await decryptAppConnectionCredentials({
orgId,
encryptedCredentials,
kmsService
});
const secretMap = await $getSecrets(secretSync);
await SecretSyncFns.sync(
{
...secretSync,
connection: {
...secretSync.connection,
credentials
}
} as TSecretSyncWithConnection,
secretMap
);
isSynced = true;
} catch (err) {
logger.error(
err,
`SecretSync Sync Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
syncErrorHistogram.record(1, {
version: 1,
destination: secretSync.destination,
syncId: secretSync.id,
projectId: secretSync.projectId,
type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
status: err instanceof AxiosError ? err.response?.status : undefined,
name: err instanceof Error ? err.name : undefined
});
}
syncMessage =
// eslint-disable-next-line no-nested-ternary
(err instanceof AxiosError
? err?.response?.data
? JSON.stringify(err?.response?.data)
: err?.message
: (err as Error)?.message) || "An unknown error occurred.";
// re-throw so job fails
throw err;
} finally {
const ranAt = new Date();
const syncStatus = isSynced ? SecretSyncStatus.Success : SecretSyncStatus.Failed;
await auditLogService.createAuditLog({
projectId: secretSync.projectId,
...(auditLogInfo ?? {
actor: {
type: ActorType.PLATFORM,
metadata: {}
}
}),
event: {
type: EventType.SYNC_SECRET_SYNC,
metadata: {
syncId: secretSync.id,
syncOptions: secretSync.syncOptions,
environment: secretSync.environment,
destination: secretSync.destination,
destinationConfig: secretSync.destinationConfig,
folderId: secretSync.folderId,
connectionId: secretSync.connectionId,
jobRanAt: ranAt,
jobId: job.id!,
syncStatus,
syncMessage
}
}
});
if (isSynced || isFinalAttempt) {
const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
syncStatus,
lastSyncJobId: job.id,
lastSyncMessage: syncMessage,
lastSyncedAt: isSynced ? ranAt : undefined
});
if (!isSynced) {
await $queueSendSecretSyncFailedNotifications({
secretSync: updatedSecretSync,
action: SecretSyncAction.Sync
});
}
}
}
logger.info("SecretSync Sync Job with ID %s Completed", job.id);
};
const $importSecrets = async (job: TSecretSyncImportDTO) => {
const {
data: { syncId, auditLogInfo, shouldOverwrite }
} = job;
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
await secretSyncDAL.updateById(syncId, {
importStatus: SecretSyncStatus.Pending
});
logger.info(
`SecretSync Import [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
);
let isImported = false;
let importMessage: string | null = null;
const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
try {
const {
connection: { orgId, encryptedCredentials },
projectId,
environment
} = secretSync;
const credentials = await decryptAppConnectionCredentials({
orgId,
encryptedCredentials,
kmsService
});
const importedSecrets = await SecretSyncFns.import({
...secretSync,
connection: {
...secretSync.connection,
credentials
}
} as TSecretSyncWithConnection);
if (Object.keys(importedSecrets).length) {
const secretMap = await $getSecrets(secretSync, false);
const secretsToCreate: Parameters<typeof $createManySecretsRawFn>[0]["secrets"] = [];
const secretsToUpdate: Parameters<typeof $updateManySecretsRawFn>[0]["secrets"] = [];
Object.entries(importedSecrets).forEach(([key, { value }]) => {
const secret = {
secretName: key,
secretValue: value,
type: SecretType.Shared,
secretComment: ""
};
if (Object.hasOwn(secretMap, key)) {
secretsToUpdate.push(secret);
} else {
secretsToCreate.push(secret);
}
});
if (secretsToCreate.length) {
await $createManySecretsRawFn({
projectId,
path: secretSync.folder.path,
environment: environment.slug,
secrets: secretsToCreate
});
}
if (shouldOverwrite && secretsToUpdate.length) {
await $updateManySecretsRawFn({
projectId,
path: secretSync.folder.path,
environment: environment.slug,
secrets: secretsToUpdate
});
}
}
isImported = true;
} catch (err) {
logger.error(
err,
`SecretSync Import Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
importErrorHistogram.record(1, {
version: 1,
destination: secretSync.destination,
syncId: secretSync.id,
projectId: secretSync.projectId,
type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
status: err instanceof AxiosError ? err.response?.status : undefined,
name: err instanceof Error ? err.name : undefined
});
}
importMessage =
// eslint-disable-next-line no-nested-ternary
(err instanceof AxiosError
? err?.response?.data
? JSON.stringify(err?.response?.data)
: err?.message
: (err as Error)?.message) || "An unknown error occurred.";
// re-throw so job fails
throw err;
} finally {
const ranAt = new Date();
const importStatus = isImported ? SecretSyncStatus.Success : SecretSyncStatus.Failed;
await auditLogService.createAuditLog({
projectId: secretSync.projectId,
...(auditLogInfo ?? {
actor: {
type: ActorType.PLATFORM,
metadata: {}
}
}),
event: {
type: EventType.IMPORT_SECRET_SYNC,
metadata: {
syncId: secretSync.id,
syncOptions: secretSync.syncOptions,
environment: secretSync.environment,
destination: secretSync.destination,
destinationConfig: secretSync.destinationConfig,
folderId: secretSync.folderId,
connectionId: secretSync.connectionId,
jobRanAt: ranAt,
jobId: job.id!,
importStatus,
importMessage
}
}
});
if (isImported || isFinalAttempt) {
const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
importStatus,
lastImportJobId: job.id,
lastImportMessage: importMessage,
lastImportedAt: isImported ? ranAt : undefined
});
if (!isImported) {
await $queueSendSecretSyncFailedNotifications({
secretSync: updatedSecretSync,
action: SecretSyncAction.Import
});
}
}
}
logger.info("SecretSync Import Job with ID %s Completed", job.id);
};
const $eraseSecrets = async (job: TSecretSyncEraseDTO) => {
const {
data: { syncId, auditLogInfo }
} = job;
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
await secretSyncDAL.updateById(syncId, {
eraseStatus: SecretSyncStatus.Pending
});
logger.info(
`SecretSync Erase [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
);
let isErased = false;
let eraseMessage: string | null = null;
const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
try {
const {
connection: { orgId, encryptedCredentials }
} = secretSync;
const credentials = await decryptAppConnectionCredentials({
orgId,
encryptedCredentials,
kmsService
});
const secretMap = await $getSecrets(secretSync);
await SecretSyncFns.erase(
{
...secretSync,
connection: {
...secretSync.connection,
credentials
}
} as TSecretSyncWithConnection,
secretMap
);
isErased = true;
} catch (err) {
logger.error(
err,
`SecretSync Erase Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
eraseErrorHistogram.record(1, {
version: 1,
destination: secretSync.destination,
syncId: secretSync.id,
projectId: secretSync.projectId,
type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
status: err instanceof AxiosError ? err.response?.status : undefined,
name: err instanceof Error ? err.name : undefined
});
}
eraseMessage =
// eslint-disable-next-line no-nested-ternary
(err instanceof AxiosError
? err?.response?.data
? JSON.stringify(err?.response?.data)
: err?.message
: (err as Error)?.message) || "An unknown error occurred.";
// re-throw so job fails
throw err;
} finally {
const ranAt = new Date();
const eraseStatus = isErased ? SecretSyncStatus.Success : SecretSyncStatus.Failed;
await auditLogService.createAuditLog({
projectId: secretSync.projectId,
...(auditLogInfo ?? {
actor: {
type: ActorType.PLATFORM,
metadata: {}
}
}),
event: {
type: EventType.ERASE_SECRET_SYNC,
metadata: {
syncId: secretSync.id,
syncOptions: secretSync.syncOptions,
environment: secretSync.environment,
destination: secretSync.destination,
destinationConfig: secretSync.destinationConfig,
folderId: secretSync.folderId,
connectionId: secretSync.connectionId,
jobRanAt: ranAt,
jobId: job.id!,
eraseStatus,
eraseMessage
}
}
});
if (isErased || isFinalAttempt) {
const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
eraseStatus,
lastEraseJobId: job.id,
lastEraseMessage: eraseMessage,
lastErasedAt: isErased ? ranAt : undefined
});
if (!isErased) {
await $queueSendSecretSyncFailedNotifications({
secretSync: updatedSecretSync,
action: SecretSyncAction.Erase
});
}
}
}
logger.info("SecretSync Erase Job with ID %s Completed", job.id);
};
const $sendSecretSyncFailedNotifications = async (job: TSendSecretSyncFailedNotificationsJobDTO) => {
const {
data: { secretSync, auditLogInfo, action }
} = job;
const { projectId, destination, name, folder, lastSyncMessage, lastEraseMessage, lastImportMessage, environment } =
secretSync;
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
const project = await projectDAL.findById(projectId);
let projectAdmins = projectMembers.filter((member) =>
member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
);
const triggeredByUserId =
auditLogInfo && auditLogInfo.actor.type === ActorType.USER && auditLogInfo.actor.metadata.userId;
// only notify triggering user if triggered by admin
if (triggeredByUserId && projectAdmins.map((admin) => admin.userId).includes(triggeredByUserId)) {
projectAdmins = projectAdmins.filter((admin) => admin.userId === triggeredByUserId);
}
const syncDestination = SECRET_SYNC_NAME_MAP[destination as SecretSync];
let subject: string;
let failureMessage: string | null | undefined;
let content: string;
switch (action) {
case SecretSyncAction.Import:
subject = "Import";
failureMessage = lastImportMessage;
content = `Your ${syncDestination} Sync named "${name}" failed while attempting to import secrets.`;
break;
case SecretSyncAction.Erase:
subject = "Erase";
failureMessage = lastEraseMessage;
content = `Your ${syncDestination} Sync named "${name}" failed while attempting to erase secrets.`;
break;
case SecretSyncAction.Sync:
default:
subject = `Sync`;
failureMessage = lastSyncMessage;
content = `Your ${syncDestination} Sync named "${name}" failed to sync.`;
break;
}
await smtpService.sendMail({
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
template: SmtpTemplates.SecretSyncFailed,
subjectLine: `Secret Sync Failed to ${subject} Secrets`,
substitutions: {
syncName: name,
syncDestination,
content,
failureMessage,
secretPath: folder.path,
environment: environment.name,
projectName: project.name,
// TODO (scott): verify this is still the URL after bare react change
syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}`
}
});
};
const queueSecretSyncsByPath = async ({ secretPath, projectId, environmentSlug }: TQueueSecretSyncsByPathDTO) => {
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath);
if (!folder)
throw new Error(
`Could not find folder at path "${secretPath}" for environment with slug "${environmentSlug}" in project with ID "${projectId}"`
);
const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isEnabled: true });
await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncById({ syncId: secretSync.id })));
};
queueService.start(QueueName.AppConnectionSecretSync, async (job) => {
if (job.name === QueueJobs.AppConnectionSendSecretSyncActionFailedNotifications) {
await $sendSecretSyncFailedNotifications(job as TSendSecretSyncFailedNotificationsJobDTO);
return;
}
const { syncId } = job.data as
| TQueueSecretSyncByIdDTO
| TQueueSecretSyncImportByIdDTO
| TQueueSecretSyncEraseByIdDTO;
const lock = await keyStore.acquireLock([KeyStorePrefixes.SecretSyncLock(syncId)], 5 * 60 * 1000);
try {
switch (job.name) {
case QueueJobs.AppConnectionSecretSync:
await $syncSecrets(job as TSecretSyncDTO);
break;
case QueueJobs.AppConnectionSecretSyncImport:
await $importSecrets(job as TSecretSyncImportDTO);
break;
case QueueJobs.AppConnectionSecretSyncErase:
await $eraseSecrets(job as TSecretSyncEraseDTO);
break;
default:
throw new InternalServerError({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
message: `Unhandled Secret Sync Job ${job.name}`
});
}
} finally {
await lock.release();
}
});
return {
queueSecretSyncById,
queueSecretSyncImportById,
queueSecretSyncEraseById,
queueSecretSyncsByPath
};
};

View File

@@ -0,0 +1,65 @@
import { z } from "zod";
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
import { SecretSyncs } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
const SyncOptionsSchema = z.object({
prependPrefix: z
.string()
.trim()
.transform((str) => str.toUpperCase())
.optional()
.describe(SecretSyncs.SYNC_OPTIONS.PREPEND_PREFIX),
appendSuffix: z
.string()
.trim()
.transform((str) => str.toUpperCase())
.optional()
.describe(SecretSyncs.SYNC_OPTIONS.APPEND_SUFFIX)
});
export const BaseSecretSyncSchema = (app: AppConnection) =>
SecretSyncsSchema.omit({
destination: true,
destinationConfig: true,
syncOptions: true
}).extend({
syncOptions: SyncOptionsSchema,
// join properties
projectId: z.string(),
connection: z.object({ app: z.literal(app), name: z.string(), id: z.string().uuid() }),
environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }),
folder: z.object({ id: z.string(), path: z.string() })
});
export const GenericCreateSecretSyncFieldsSchema = (sync: SecretSync) =>
z.object({
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(sync).name),
description: z
.string()
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(SecretSyncs.CREATE(sync).description),
connectionId: z.string().uuid().describe(SecretSyncs.CREATE(sync).connectionId),
folderId: z.string().uuid().describe(SecretSyncs.CREATE(sync).folderId),
isEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(sync).isEnabled),
syncOptions: SyncOptionsSchema.optional().default({}).describe(SecretSyncs.CREATE(sync).syncOptions)
});
export const GenericUpdateSecretSyncFieldsSchema = (sync: SecretSync) =>
z.object({
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(sync).name).optional(),
description: z
.string()
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(SecretSyncs.UPDATE(sync).description),
folderId: z.string().uuid().optional().describe(SecretSyncs.UPDATE(sync).folderId),
isEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(sync).isEnabled),
syncOptions: SyncOptionsSchema.optional().describe(SecretSyncs.UPDATE(sync).syncOptions)
});

View File

@@ -0,0 +1,472 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { startsWithVowel } from "@app/lib/fn";
import { OrgServiceActor } from "@app/lib/types";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns";
import {
TCreateSecretSyncDTO,
TDeleteSecretSyncDTO,
TFindSecretSyncByIdDTO,
TFindSecretSyncByNameDTO,
TListSecretSyncsByProjectId,
TSecretSync,
TTriggerSecretSyncByIdDTO,
TTriggerSecretSyncEraseByIdDTO,
TTriggerSecretSyncImportByIdDTO,
TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types";
import { TSecretSyncDALFactory } from "./secret-sync-dal";
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
import { TSecretSyncQueueFactory } from "./secret-sync-queue";
type TSecretSyncServiceFactoryDep = {
secretSyncDAL: TSecretSyncDALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById">;
keyStore: Pick<TKeyStoreFactory, "getItem">;
secretSyncQueue: Pick<
TSecretSyncQueueFactory,
"queueSecretSyncById" | "queueSecretSyncImportById" | "queueSecretSyncEraseById"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; // TODO: remove once launched
};
export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFactory>;
export const secretSyncServiceFactory = ({
secretSyncDAL,
folderDAL,
licenseService,
permissionService,
appConnectionService,
projectBotService,
secretSyncQueue,
keyStore
}: TSecretSyncServiceFactoryDep) => {
// secret syncs are disabled for public until launch
const checkSecretSyncAvailability = async (orgId: string) => {
const subscription = await licenseService.getPlan(orgId);
if (!subscription.appConnections) throw new BadRequestError({ message: "Secret Syncs are not available yet." });
};
const listSecretSyncsByProjectId = async (
{ projectId, destination }: TListSecretSyncsByProjectId,
actor: OrgServiceActor
) => {
await checkSecretSyncAvailability(actor.orgId);
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
const folders = await folderDAL.findByProjectId(projectId);
const secretSyncs = await secretSyncDAL.find({
...(destination && { destination }),
$in: {
folderId: folders.map((folder) => folder.id)
}
});
return secretSyncs as TSecretSync[];
};
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
await checkSecretSyncAvailability(actor.orgId);
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync)
throw new NotFoundError({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
});
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
secretSync.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
});
return secretSync as TSecretSync;
};
const findSecretSyncByName = async (
{ destination, syncName, projectId }: TFindSecretSyncByNameDTO,
actor: OrgServiceActor
) => {
await checkSecretSyncAvailability(actor.orgId);
const folders = await folderDAL.findByProjectId(projectId);
// we prevent conflicting names within a project so this will only return one at most
const [secretSync] = await secretSyncDAL.find({
name: syncName,
$in: {
folderId: folders.map((folder) => folder.id)
}
});
if (!secretSync)
throw new NotFoundError({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with name "${syncName}"`
});
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
secretSync.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
});
return secretSync as TSecretSync;
};
const createSecretSync = async (params: TCreateSecretSyncDTO, actor: OrgServiceActor) => {
await checkSecretSyncAvailability(actor.orgId);
const folder = await folderDAL.findById(params.folderId);
if (!folder) throw new BadRequestError({ message: `Could not find Folder with ID "${params.folderId}"` });
const { permission: projectPermission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
folder.projectId,
actor.authMethod,
actor.orgId
);
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(folder.projectId);
if (!shouldUseSecretV2Bridge)
throw new BadRequestError({ message: "Project version does not support Secret Syncs" });
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretSyncs
);
const appConnection = await appConnectionService.connectAppConnectionById(params.connectionId, actor);
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
if (appConnection.app !== destinationApp) {
const appName = APP_CONNECTION_NAME_MAP[appConnection.app];
throw new BadRequestError({
message: `Invalid App Connection - Cannot sync to ${SECRET_SYNC_NAME_MAP[params.destination]} using ${
startsWithVowel(appName) ? "an" : "a"
} ${appName} Connection`
});
}
const projectFolders = await folderDAL.findByProjectId(folder.projectId);
const secretSync = await secretSyncDAL.transaction(async (tx) => {
const isConflictingName = Boolean(
(
await secretSyncDAL.find(
{
name: params.name,
$in: {
folderId: projectFolders.map((f) => f.id)
}
},
tx
)
).length
);
if (isConflictingName)
throw new BadRequestError({
message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
});
const sync = await secretSyncDAL.create(params);
return sync;
});
if (secretSync.isEnabled) await secretSyncQueue.queueSecretSyncById({ syncId: secretSync.id });
return secretSync as TSecretSync;
};
const updateSecretSync = async ({ destination, syncId, ...params }: TUpdateSecretSyncDTO, actor: OrgServiceActor) => {
await checkSecretSyncAvailability(actor.orgId);
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync)
throw new NotFoundError({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID ${syncId}`
});
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
secretSync.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretSyncs);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
});
const updatedSecretSync = await secretSyncDAL.transaction(async (tx) => {
if (params.folderId) {
const newFolder = await folderDAL.findById(params.folderId);
if (!newFolder) throw new BadRequestError({ message: `Could not find folder with ID "${params.folderId}"` });
// TODO (scott): I don't think there's a reason we can't allow moving syncs across projects
// but not supporting this initially
if (newFolder.projectId !== secretSync.projectId)
throw new BadRequestError({
message: `Cannot move Secret Sync to different project`
});
}
if (params.name && secretSync.name !== params.name) {
const projectFolders = await folderDAL.findByProjectId(secretSync.projectId);
const isConflictingName = Boolean(
(
await secretSyncDAL.find(
{
name: params.name,
$in: {
folderId: projectFolders.map((f) => f.id)
}
},
tx
)
).length
);
if (isConflictingName)
throw new BadRequestError({
message: `A Secret Sync with the name "${params.name}" already exists for project with ID "${secretSync.projectId}"`
});
}
const updatedSync = await secretSyncDAL.updateById(syncId, params);
return updatedSync;
});
if (updatedSecretSync.isEnabled) await secretSyncQueue.queueSecretSyncById({ syncId: secretSync.id });
return updatedSecretSync as TSecretSync;
};
const deleteSecretSync = async ({ destination, syncId }: TDeleteSecretSyncDTO, actor: OrgServiceActor) => {
await checkSecretSyncAvailability(actor.orgId);
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync)
throw new NotFoundError({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
});
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
secretSync.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretSyncs);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
});
await secretSyncDAL.deleteById(syncId);
return secretSync as TSecretSync;
};
const triggerSecretSyncById = async (
{ syncId, destination, ...params }: TTriggerSecretSyncByIdDTO,
actor: OrgServiceActor
) => {
await checkSecretSyncAvailability(actor.orgId);
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync)
throw new NotFoundError({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
});
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
secretSync.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
});
await secretSyncQueue.queueSecretSyncById({ syncId, ...params });
return secretSync as TSecretSync;
};
const triggerSecretSyncImportById = async (
{ syncId, destination, ...params }: TTriggerSecretSyncImportByIdDTO,
actor: OrgServiceActor
) => {
await checkSecretSyncAvailability(actor.orgId);
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync)
throw new NotFoundError({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
});
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
secretSync.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
});
const isSyncJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretSyncLock(syncId)));
if (isSyncJobRunning)
throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
await secretSyncQueue.queueSecretSyncImportById({ syncId, ...params });
return secretSync as TSecretSync;
};
const triggerSecretSyncEraseById = async (
{ syncId, destination, ...params }: TTriggerSecretSyncEraseByIdDTO,
actor: OrgServiceActor
) => {
await checkSecretSyncAvailability(actor.orgId);
const secretSync = await secretSyncDAL.findById(syncId);
if (!secretSync)
throw new NotFoundError({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
});
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
secretSync.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
});
const isSyncJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretSyncLock(syncId)));
if (isSyncJobRunning)
throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
await secretSyncQueue.queueSecretSyncEraseById({ syncId, ...params });
return secretSync as TSecretSync;
};
return {
listSecretSyncOptions,
listSecretSyncsByProjectId,
findSecretSyncById,
findSecretSyncByName,
createSecretSync,
updateSecretSync,
deleteSecretSync,
triggerSecretSyncById,
triggerSecretSyncImportById,
triggerSecretSyncEraseById
};
};

View File

@@ -0,0 +1,131 @@
import { Job } from "bullmq";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { QueueJobs } from "@app/queue";
import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
TAwsParameterStoreSync,
TAwsParameterStoreSyncInput,
TAwsParameterStoreSyncListItem,
TAwsParameterStoreSyncWithConnection
} from "./aws-parameter-store";
export type TSecretSync = TAwsParameterStoreSync;
export type TSecretSyncWithConnection = TAwsParameterStoreSyncWithConnection;
export type TSecretSyncInput = TAwsParameterStoreSyncInput;
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem;
export type TListSecretSyncsByProjectId = {
projectId: string;
destination?: SecretSync;
};
export type TFindSecretSyncByIdDTO = {
syncId: string;
destination: SecretSync;
};
export type TFindSecretSyncByNameDTO = {
syncName: string;
projectId: string;
destination: SecretSync;
};
export type TCreateSecretSyncDTO = Pick<
TSecretSync,
"syncOptions" | "destinationConfig" | "folderId" | "name" | "connectionId"
> & { destination: SecretSync };
export type TUpdateSecretSyncDTO = Partial<Omit<TCreateSecretSyncDTO, "connectionId">> & {
syncId: string;
destination: SecretSync;
};
export type TDeleteSecretSyncDTO = {
destination: SecretSync;
syncId: string;
};
type AuditLogInfo = Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
export enum SecretSyncStatus {
Pending = "pending",
Success = "success",
Failed = "failed"
}
export enum SecretSyncAction {
Sync = "sync",
Import = "import",
Erase = "erase"
}
export type TSecretSyncRaw = NonNullable<Awaited<ReturnType<TSecretSyncDALFactory["findById"]>>>;
export type TQueueSecretSyncsByPathDTO = {
secretPath: string;
environmentSlug: string;
projectId: string;
};
export type TQueueSecretSyncByIdDTO = {
syncId: string;
auditLogInfo?: AuditLogInfo;
};
export type TTriggerSecretSyncByIdDTO = {
destination: SecretSync;
} & TQueueSecretSyncByIdDTO;
export type TQueueSecretSyncImportByIdDTO = {
syncId: string;
shouldOverwrite: boolean;
auditLogInfo?: AuditLogInfo;
};
export type TTriggerSecretSyncImportByIdDTO = {
destination: SecretSync;
} & TQueueSecretSyncImportByIdDTO;
export type TQueueSecretSyncEraseByIdDTO = {
syncId: string;
auditLogInfo?: AuditLogInfo;
};
export type TTriggerSecretSyncEraseByIdDTO = {
destination: SecretSync;
} & TQueueSecretSyncEraseByIdDTO;
export type TQueueSendSecretSyncActionFailedNotificationsDTO = {
secretSync: TSecretSyncRaw;
auditLogInfo?: AuditLogInfo;
action: SecretSyncAction;
};
export type TSecretSyncDTO = Job<TQueueSecretSyncByIdDTO, void, QueueJobs.AppConnectionSecretSync>;
export type TSecretSyncImportDTO = Job<TQueueSecretSyncImportByIdDTO, void, QueueJobs.AppConnectionSecretSync>;
export type TSecretSyncEraseDTO = Job<TQueueSecretSyncEraseByIdDTO, void, QueueJobs.AppConnectionSecretSync>;
export type TSendSecretSyncFailedNotificationsJobDTO = Job<
TQueueSendSecretSyncActionFailedNotificationsDTO,
void,
QueueJobs.AppConnectionSendSecretSyncActionFailedNotifications
>;
export type TSecretMap = Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
>;
export type TSecretSyncGetSecrets = {
projectId: string;
folderId: string;
secretPath: string;
environmentSlug: string;
includeImports?: boolean;
};

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-await-in-loop */
import opentelemetry from "@opentelemetry/api";
import { AxiosError } from "axios";
import {
@@ -28,6 +29,7 @@ import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@a
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-queue";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { ActorType } from "../auth/auth-type";
@@ -103,6 +105,7 @@ type TSecretQueueFactoryDep = {
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
secretSyncQueue: Pick<TSecretSyncQueueFactory, "queueSecretSyncsByPath">;
};
export type TGetSecrets = {
@@ -156,8 +159,15 @@ export const secretQueueFactory = ({
auditLogService,
orgService,
projectUserMembershipRoleDAL,
projectKeyDAL
projectKeyDAL,
secretSyncQueue
}: TSecretQueueFactoryDep) => {
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
description: "Integration secret sync errors",
unit: "1"
});
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const appCfg = getConfig();
await queueService.stopRepeatableJob(
@@ -612,6 +622,9 @@ export const secretQueueFactory = ({
}
}
);
await secretSyncQueue.queueSecretSyncsByPath({ projectId, environmentSlug: environment, secretPath });
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false });
if (!excludeReplication) {
await replicateSecrets({
@@ -933,6 +946,19 @@ export const secretQueueFactory = ({
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]`
);
const appCfg = getConfig();
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
errorHistogram.record(1, {
version: 1,
integration: integration.integration,
integrationId: integration.id,
type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
status: err instanceof AxiosError ? err.response?.status : undefined,
name: err instanceof Error ? err.name : undefined,
projectId: integration.projectId
});
}
const message =
// eslint-disable-next-line no-nested-ternary
(err instanceof AxiosError

View File

@@ -51,7 +51,7 @@ const buildSlackPayload = (notification: TSlackNotification) => {
*Environment*: ${payload.environment}
*Secret path*: ${payload.secretPath || "/"}
View the complete details <${appCfg.SITE_URL}/project/${payload.projectId}/approval?requestId=${
View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${
payload.requestId
}|here>.`;

View File

@@ -35,6 +35,7 @@ export enum SmtpTemplates {
ScimUserProvisioned = "scimUserProvisioned.handlebars",
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
IntegrationSyncFailed = "integrationSyncFailed.handlebars",
SecretSyncFailed = "secretSyncFailed.handlebars",
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
ExternalImportFailed = "externalImportFailed.handlebars",
ExternalImportStarted = "externalImportStarted.handlebars"

View File

@@ -0,0 +1,35 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>{{syncDestination}} Sync "{{syncName}}" Failed</title>
</head>
<body>
<h2>Infisical</h2>
<div>
<p>{{content}}</p>
<a href="{{syncUrl}}">
View in Infisical.
</a>
</div>
<br />
<div>
<p><strong>Name</strong>: {{syncName}}</p>
<p><strong>Destination</strong>: {{syncDestination}}</p>
<p><strong>Project</strong>: {{projectName}}</p>
<p><strong>Environment</strong>: {{environment}}</p>
<p><strong>Secret Path</strong>: {{secretPath}}</p>
</div>
{{#if failureMessage}}
<p><b>Reason: </b>{{failureMessage}}</p>
{{/if}}
{{emailFooter}}
</body>
</html>

View File

@@ -19,7 +19,7 @@ Every new joiner has an onboarding buddy who should ideally be in the the same t
1. Join the weekly all-hands meeting. It typically happens on Monday's at 8:30am PT.
2. Ship something together on day one even if tiny! It feels great to hit the ground running, with a development environment all ready to go.
3. Check out the [Areas of Responsibility (AoR) Table](https://docs.google.com/spreadsheets/d/1RnXlGFg83Sgu0dh7ycuydsSobmFfI3A0XkGw7vrVxEI/edit?usp=sharing). This is helpful to know who you can ask about particular areas of Infisical. Feel free to add yourself to the areas you'd be most interesting to dive into.
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1RaJd3RoS2QpWLFHlgfHaXnHqCCwRt6mCGZkbJ75J_D0/edit?usp=sharing).
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1uV9IaahYwbZ5OuzDTFdQMSa1P0mpMOnetGB-xqf4G40).
5. Update your LinkedIn profile with one of [Infisical's official banners](https://drive.google.com/drive/u/0/folders/1oSNWjbpRl9oNYwxM_98IqzKs9fAskrb2) (if you want to). You can also coordinate your social posts in the #marketing Slack channel, so that we can boost it from Infisical's official social media accounts.
6. Over the first few weeks, feel free to schedule 1:1s with folks on the team to get to know them a bit better.
7. Change your Slack username in the users channel to `[NAME] (Infisical)`.

View File

@@ -144,9 +144,6 @@ services:
- ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files
- ./frontend/public:/app/public
env_file: .env
environment:
- NEXT_PUBLIC_ENV=development
- INFISICAL_TELEMETRY_ENABLED=false
pgadmin:
image: dpage/pgadmin4

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/aws/available"
---

View File

@@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/aws"
---
<Note>
Check out the configuration docs for [AWS Connections](/integrations/app-connections/aws) to learn how to obtain
the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/aws/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/aws/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/aws/name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/aws"
---

View File

@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/aws/{connectionId}"
---
<Note>
Check out the configuration docs for [AWS Connections](/integrations/app-connections/aws) to learn how to obtain
the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/github/available"
---

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/github"
---
<Note>
GitHub Connections must be created through the Infisical UI.
Check out the configuration docs for [GitHub Connections](/integrations/app-connections/github) for a step-by-step
guide.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/github/{connectionId}"
---

Some files were not shown because too many files have changed in this diff Show More