Compare commits

...

138 Commits

Author SHA1 Message Date
a74f0170da Merge pull request #2954 from Infisical/fix/addressed-reported-migration-related-ui-issues
fix: resolved conditions permission not getting added from new policy…
2025-01-08 22:26:45 +08:00
=
a0fad34a6d fix: resolved conditions permission not getting added from new policy button 2025-01-08 19:48:13 +05:30
0b9d890a51 Merge pull request #2953 from Infisical/fix/addressed-reported-migration-related-ui-issues
fix: address sso redirect and project role creation
2025-01-08 19:20:19 +08:00
5ba507bc1c misc: made installation ID optional for github 2025-01-08 19:17:38 +08:00
=
0ecc196e5d feat: added missing coerce in azure key vault 2025-01-08 16:30:46 +05:30
=
ddac9f7cc4 feat: made all redirect route to coerce it 2025-01-08 16:27:56 +05:30
34354994d8 fix: address sso redirect and project role creation 2025-01-08 18:29:25 +08:00
1576358805 Merge pull request #2947 from Infisical/auth0-saml
Add Support for Auth0 SAML
2025-01-07 15:04:44 -05:00
e6103d2d3f docs: update initial saml setup steps 2025-01-07 11:45:07 -08:00
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
6d3793beff Merge pull request #2949 from akhilmhdh/fix/broken-image
Fixed broke image
2025-01-08 00:18:38 +05:30
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
0cefd6f837 Run linter 2025-01-08 01:41:22 +07:00
5e9dc0b98d clarify enterprise plan for user group feature upgrade modal 2025-01-07 10:38:09 -08:00
f632847dc6 Merge branch 'main', remote-tracking branch 'origin' into auth0-saml 2025-01-08 01:35:11 +07:00
faa6d1cf40 Add support for Auth0 SAML 2025-01-08 01:34:03 +07:00
7fb18870e3 Merge pull request #2946 from akhilmhdh/feat/updated-saml-error-message
Updated saml error message
2025-01-07 10:57:03 -05:00
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
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
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
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
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
00490f2cff misc: made secret path input show correct folders based on env in integrations 2025-01-07 12:08:09 +08:00
ee58f538c0 feat: target specific azure key vault tenant 2025-01-07 11:53:17 +08:00
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
40ef75d3bd fix: use force flag when deleting secrets from aws secret manager integration 2025-01-06 16:11:32 -08:00
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
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
d2e3f504fd misc: updated openssh installation for fips 2025-01-07 03:04:57 +08:00
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
96b8e7fda8 Merge pull request #2930 from Infisical/vmatsiiako-wiki-patch-1 2025-01-01 18:12:48 -05:00
93b9108aa3 Update onboarding.mdx 2025-01-01 15:04:50 -08:00
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
f32588112e fix: resolved azure app config api not pulling all keys 2024-12-29 19:51:59 +05:30
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
b45d9398f0 Merge pull request #2920 from Infisical/vmatsiiako-intercom-patch-1 2024-12-27 21:47:36 -05:00
1d1140237f Update azure-app-configuration.mdx 2024-12-27 00:59:49 -05:00
937560fd8d Update azure-app-configuration.mdx 2024-12-27 00:58:48 -05:00
5f4b7b9ea7 Merge pull request #2921 from Infisical/patch-azure-label
Azure App Config Label patch
2024-12-27 00:40:42 -05:00
05139820a5 add docs for label and refereces 2024-12-26 16:36:27 -05:00
7f6bc3ecfe misc: added additional note for azure app config 2024-12-26 19:07:45 +08:00
d8cc000ad1 feat: authenticate key vault against specific tenant 2024-12-26 19:07:16 +08:00
8fc03c06d9 handle empty label 2024-12-26 01:17:47 -05:00
50ceedf39f Remove intercom from docs 2024-12-25 16:40:45 -08:00
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
1190ca2d77 docs: add note Approval Workflows as paid feature 2024-12-25 15:27:38 +07:00
2fb60201bc add not for dynamic secrets as paid feature 2024-12-25 14:17:27 +07:00
634b500244 Merge pull request #2907 from Infisical/temp-hide-app-connection-docs 2024-12-20 18:53:20 -05:00
54b4d4ae55 docs: temp hide app connections 2024-12-20 15:07:23 -08:00
2f6dab3f63 Merge pull request #2901 from Infisical/ssh-cli-docs
Documentation for SSH Command in CLI
2024-12-20 08:49:38 -08:00
e9564f5231 Fix ssh cli docs based on review 2024-12-19 22:12:32 -08:00
05cdca9202 Add docs for SSH CLI 2024-12-19 19:47:24 -08:00
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
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
2357f3bc1f misc: added integration ID 2024-12-20 03:10:59 +08:00
cde813aafb misc: added custom metrics for integration syncs 2024-12-20 03:08:55 +08:00
bbc8091d44 misc: added metric for api errors 2024-12-20 02:37:44 +08:00
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
5ae74f9761 Update create.tsx 2024-12-19 18:53:17 +01:00
eef331bbd1 Merge pull request #2870 from Infisical/app-connections
Feat: App Connections
2024-12-19 09:51:38 -08:00
d5c2e9236a fix: doc typo 2024-12-19 09:43:14 -08:00
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
3fa84c578c fix: correct role description schema to accept null 2024-12-18 22:15:40 -08:00
c22ed04733 fix: correct imports to use alias 2024-12-18 21:30:36 -08:00
64fac1979f revert: mint and license 2024-12-18 21:21:28 -08:00
2d60f389c2 improvements: address feedback 2024-12-18 21:18:38 -08:00
7798e5a2ad fix: default behavior 2024-12-19 01:25:29 +01:00
ed78227725 fix(vercel-integration): initial sync logic 2024-12-19 01:18:47 +01:00
62968c5e43 merge main 2024-12-18 10:09:23 -08:00
7947e73569 fix: correct import path to use alias 2024-12-17 09:21:23 -08:00
52ce90846a feature: app connections 2024-12-16 22:46:08 -08:00
242595fceb changed getCaCerts key from ca-cert -> ca-certs 2024-11-24 21:05:39 +05:30
1544 changed files with 38687 additions and 42314 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";
@ -208,6 +209,7 @@ declare module "fastify" {
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
projectTemplate: TProjectTemplateServiceFactory;
totp: TTotpServiceFactory;
appConnection: TAppConnectionServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@ -363,6 +363,7 @@ import {
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections";
import {
TExternalGroupOrgRoleMappings,
TExternalGroupOrgRoleMappingsInsert,
@ -886,5 +887,10 @@ declare module "knex/types/tables" {
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate
>;
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
TAppConnections,
TAppConnectionsInsert,
TAppConnectionsUpdate
>;
}
}

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,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,8 @@ export enum TableName {
KmsKeyVersion = "kms_key_versions",
WorkflowIntegrations = "workflow_integrations",
SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs"
ProjectSlackConfigs = "project_slack_configs",
AppConnection = "app_connections"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@ -23,7 +23,7 @@ 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(),
description: z.string().trim().nullish(),
permissions: z.any().array()
}),
response: {
@ -95,7 +95,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
)
.optional(),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
description: z.string().trim().nullish(),
permissions: z.any().array().optional()
}),
response: {

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

@ -6,6 +6,8 @@ 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";
@ -222,7 +224,12 @@ 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_APP_CONNECTION = "get-app-connection",
CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection"
}
interface UserActorMetadata {
@ -1867,6 +1874,39 @@ interface ApplyProjectTemplateEvent {
};
}
interface GetAppConnectionsEvent {
type: EventType.GET_APP_CONNECTIONS;
metadata: {
app?: AppConnection;
count: number;
connectionIds: string[];
};
}
interface GetAppConnectionEvent {
type: EventType.GET_APP_CONNECTION;
metadata: {
connectionId: string;
};
}
interface CreateAppConnectionEvent {
type: EventType.CREATE_APP_CONNECTION;
metadata: Omit<TCreateAppConnectionDTO, "credentials"> & { connectionId: string };
}
interface UpdateAppConnectionEvent {
type: EventType.UPDATE_APP_CONNECTION;
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean };
}
interface DeleteAppConnectionEvent {
type: EventType.DELETE_APP_CONNECTION;
metadata: {
connectionId: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -2038,4 +2078,9 @@ export type Event =
| CreateProjectTemplateEvent
| UpdateProjectTemplateEvent
| DeleteProjectTemplateEvent
| ApplyProjectTemplateEvent;
| ApplyProjectTemplateEvent
| GetAppConnectionsEvent
| GetAppConnectionEvent
| CreateAppConnectionEvent
| UpdateAppConnectionEvent
| DeleteAppConnectionEvent;

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

@ -27,7 +27,8 @@ export enum OrgPermissionSubjects {
Kms = "kms",
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates"
ProjectTemplates = "project-templates",
AppConnections = "app-connections"
}
export type OrgPermissionSet =
@ -46,6 +47,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => {
@ -123,6 +125,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return rules;
@ -153,6 +160,8 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
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

@ -1,3 +1,6 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
export const GROUPS = {
CREATE: {
name: "The name of the group to create.",
@ -1605,3 +1608,34 @@ 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.`
})
};

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,5 @@ export const prefixWithSlash = (str: string) => {
if (str.startsWith("/")) return str;
return `/${str}`;
};
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);

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

@ -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";
@ -314,6 +316,7 @@ export const registerRoutes = async (
const auditLogStreamDAL = auditLogStreamDALFactory(db);
const trustedIpDAL = trustedIpDALFactory(db);
const telemetryDAL = telemetryDALFactory(db);
const appConnectionDAL = appConnectionDALFactory(db);
// ee db layer ops
const permissionDAL = permissionDALFactory(db);
@ -1352,6 +1355,13 @@ export const registerRoutes = async (
externalGroupOrgRoleMappingDAL
});
const appConnectionService = appConnectionServiceFactory({
appConnectionDAL,
permissionService,
kmsService,
licenseService
});
await superAdminService.initServerCfg();
// setup the communication with license key server
@ -1448,7 +1458,8 @@ export const registerRoutes = async (
migration: migrationService,
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
projectTemplate: projectTemplateService,
totp: totpService
totp: totpService,
appConnection: appConnectionService
});
const cronJobs: CronJob[] = [];

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,274 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { AppConnections } from "@app/lib/api-docs";
import { startsWithVowel } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
server,
app,
createSchema,
updateSchema,
responseSchema
}: {
app: AppConnection;
server: FastifyZodProvider;
createSchema: z.ZodType<{
name: string;
method: I["method"];
credentials: I["credentials"];
description?: string | null;
}>;
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
responseSchema: z.ZodTypeAny;
}) => {
const appName = APP_CONNECTION_NAME_MAP[app];
server.route({
method: "GET",
url: `/`,
config: {
rateLimit: readLimit
},
schema: {
description: `List the ${appName} Connections for the current organization.`,
response: {
200: z.object({ appConnections: responseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[];
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_APP_CONNECTIONS,
metadata: {
app,
count: appConnections.length,
connectionIds: appConnections.map((connection) => connection.id)
}
}
});
return { appConnections };
}
});
server.route({
method: "GET",
url: "/:connectionId",
config: {
rateLimit: readLimit
},
schema: {
description: `Get the specified ${appName} Connection by ID.`,
params: z.object({
connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId)
}),
response: {
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.findAppConnectionById(
app,
connectionId,
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_APP_CONNECTION,
metadata: {
connectionId
}
}
});
return { appConnection };
}
});
server.route({
method: "GET",
url: `/name/:connectionName`,
config: {
rateLimit: readLimit
},
schema: {
description: `Get the specified ${appName} Connection by name.`,
params: z.object({
connectionName: z
.string()
.min(0, "Connection name required")
.describe(AppConnections.GET_BY_NAME(app).connectionName)
}),
response: {
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionName } = req.params;
const appConnection = (await server.services.appConnection.findAppConnectionByName(
app,
connectionName,
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_APP_CONNECTION,
metadata: {
connectionId: appConnection.id
}
}
});
return { appConnection };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: `Create ${
startsWithVowel(appName) ? "an" : "a"
} ${appName} Connection for the current organization.`,
body: createSchema,
response: {
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, method, credentials, description } = req.body;
const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials, description },
req.permission
)) as TAppConnection;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_APP_CONNECTION,
metadata: {
name,
method,
app,
connectionId: appConnection.id
}
}
});
return { appConnection };
}
});
server.route({
method: "PATCH",
url: "/:connectionId",
config: {
rateLimit: writeLimit
},
schema: {
description: `Update the specified ${appName} Connection.`,
params: z.object({
connectionId: z.string().uuid().describe(AppConnections.UPDATE(app).connectionId)
}),
body: updateSchema,
response: {
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, credentials, description } = req.body;
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.updateAppConnection(
{ name, credentials, connectionId, description },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_APP_CONNECTION,
metadata: {
name,
description,
credentialsUpdated: Boolean(credentials),
connectionId
}
}
});
return { appConnection };
}
});
server.route({
method: "DELETE",
url: `/:connectionId`,
config: {
rateLimit: writeLimit
},
schema: {
description: `Delete the specified ${appName} Connection.`,
params: z.object({
connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId)
}),
response: {
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.deleteAppConnection(
app,
connectionId,
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_APP_CONNECTION,
metadata: {
connectionId
}
}
});
return { appConnection };
}
});
};

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

View File

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

View File

@ -0,0 +1,2 @@
export * from "./app-connection-router";
export * from "./apps";

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,3 +1,4 @@
import { APP_CONNECTION_REGISTER_MAP, registerAppConnectionRouter } from "@app/server/routes/v1/app-connection-routers";
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
@ -110,4 +111,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
await server.register(registerCmekRouter, { prefix: "/kms" });
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
await server.register(
async (appConnectionsRouter) => {
await appConnectionsRouter.register(registerAppConnectionRouter);
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_MAP)) {
await appConnectionsRouter.register(router, { prefix: `/${app}` });
}
},
{ prefix: "/app-connections" }
);
};

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,4 @@
export enum AppConnection {
GitHub = "github",
AWS = "aws"
}

View File

@ -0,0 +1,92 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
import {
AwsConnectionMethod,
getAwsAppConnectionListItem,
validateAwsConnectionCredentials
} from "@app/services/app-connection/aws";
import {
getGitHubConnectionListItem,
GitHubConnectionMethod,
validateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
import { KmsDataKey } from "@app/services/kms/kms-types";
export const listAppConnectionOptions = () => {
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
};
export const encryptAppConnectionCredentials = async ({
orgId,
credentials,
kmsService
}: {
orgId: string;
credentials: TAppConnection["credentials"];
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
}) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
plainText: Buffer.from(JSON.stringify(credentials))
});
return encryptedCredentialsBlob;
};
export const decryptAppConnectionCredentials = async ({
orgId,
encryptedCredentials,
kmsService
}: {
orgId: string;
encryptedCredentials: Buffer;
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
}) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const decryptedPlainTextBlob = decryptor({
cipherTextBlob: encryptedCredentials
});
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
};
export const validateAppConnectionCredentials = async (
appConnection: TAppConnectionConfig
): Promise<TAppConnection["credentials"]> => {
const { app } = appConnection;
switch (app) {
case AppConnection.AWS: {
return validateAwsConnectionCredentials(appConnection);
}
case AppConnection.GitHub:
return validateGitHubConnectionCredentials(appConnection);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection ${app}`);
}
};
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
switch (method) {
case GitHubConnectionMethod.App:
return "GitHub App";
case GitHubConnectionMethod.OAuth:
return "OAuth";
case AwsConnectionMethod.AccessKey:
return "Access Key";
case AwsConnectionMethod.AssumeRole:
return "Assume Role";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
}
};

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,360 @@
import { ForbiddenError } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
decryptAppConnectionCredentials,
encryptAppConnectionCredentials,
getAppConnectionMethodName,
listAppConnectionOptions,
validateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-fns";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import {
TAppConnection,
TAppConnectionConfig,
TCreateAppConnectionDTO,
TUpdateAppConnectionDTO,
TValidateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-types";
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal";
export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; // TODO: remove once launched
};
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
appConnectionDAL,
permissionService,
kmsService,
licenseService
}: TAppConnectionServiceFactoryDep) => {
// app connections are disabled for public until launch
const checkAppServicesAvailability = async (orgId: string) => {
const subscription = await licenseService.getPlan(orgId);
if (!subscription.appConnections) throw new BadRequestError({ message: "App Connections are not available yet." });
};
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
await checkAppServicesAvailability(actor.orgId);
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
const appConnections = await appConnectionDAL.find(
app
? { orgId: actor.orgId, app }
: {
orgId: actor.orgId
}
);
return Promise.all(
appConnections
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
.map(async ({ encryptedCredentials, ...connection }) => {
const credentials = await decryptAppConnectionCredentials({
encryptedCredentials,
kmsService,
orgId: connection.orgId
});
return {
...connection,
credentials
} as TAppConnection;
})
);
};
const findAppConnectionById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
await checkAppServicesAvailability(actor.orgId);
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
appConnection.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
return {
...appConnection,
credentials: await decryptAppConnectionCredentials({
encryptedCredentials: appConnection.encryptedCredentials,
orgId: appConnection.orgId,
kmsService
})
} as TAppConnection;
};
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => {
await checkAppServicesAvailability(actor.orgId);
const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId });
if (!appConnection)
throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
appConnection.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
return {
...appConnection,
credentials: await decryptAppConnectionCredentials({
encryptedCredentials: appConnection.encryptedCredentials,
orgId: appConnection.orgId,
kmsService
})
} as TAppConnection;
};
const createAppConnection = async (
{ method, app, credentials, ...params }: TCreateAppConnectionDTO,
actor: OrgServiceActor
) => {
await checkAppServicesAvailability(actor.orgId);
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
const appConnection = await appConnectionDAL.transaction(async (tx) => {
const isConflictingName = Boolean(
await appConnectionDAL.findOne(
{
name: params.name,
orgId: actor.orgId
},
tx
)
);
if (isConflictingName)
throw new BadRequestError({
message: `An App Connection with the name "${params.name}" already exists`
});
const validatedCredentials = await validateAppConnectionCredentials({
app,
credentials,
method,
orgId: actor.orgId
} as TAppConnectionConfig);
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
const connection = await appConnectionDAL.create(
{
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
},
tx
);
return {
...connection,
credentials: validatedCredentials
};
});
return appConnection;
};
const updateAppConnection = async (
{ connectionId, credentials, ...params }: TUpdateAppConnectionDTO,
actor: OrgServiceActor
) => {
await checkAppServicesAvailability(actor.orgId);
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
appConnection.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
if (params.name && appConnection.name !== params.name) {
const isConflictingName = Boolean(
await appConnectionDAL.findOne(
{
name: params.name,
orgId: appConnection.orgId
},
tx
)
);
if (isConflictingName)
throw new BadRequestError({
message: `An App Connection with the name "${params.name}" already exists`
});
}
let encryptedCredentials: undefined | Buffer;
if (credentials) {
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
if (
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
method,
credentials
}).success
)
throw new BadRequestError({
message: `Invalid credential format for ${
APP_CONNECTION_NAME_MAP[app]
} Connection with method ${getAppConnectionMethodName(method)}`
});
const validatedCredentials = await validateAppConnectionCredentials({
app,
orgId: actor.orgId,
credentials,
method
} as TAppConnectionConfig);
if (!validatedCredentials)
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
}
const updatedConnection = await appConnectionDAL.updateById(
connectionId,
{
orgId: actor.orgId,
encryptedCredentials,
...params
},
tx
);
return updatedConnection;
});
return {
...updatedAppConnection,
credentials: await decryptAppConnectionCredentials({
encryptedCredentials: updatedAppConnection.encryptedCredentials,
orgId: updatedAppConnection.orgId,
kmsService
})
} as TAppConnection;
};
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
await checkAppServicesAvailability(actor.orgId);
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
appConnection.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
// TODO: specify delete error message if due to existing dependencies
const deletedAppConnection = await appConnectionDAL.deleteById(connectionId);
return {
...deletedAppConnection,
credentials: await decryptAppConnectionCredentials({
encryptedCredentials: deletedAppConnection.encryptedCredentials,
orgId: deletedAppConnection.orgId,
kmsService
})
} as TAppConnection;
};
return {
listAppConnectionOptions,
listAppConnectionsByOrg,
findAppConnectionById,
findAppConnectionByName,
createAppConnection,
updateAppConnection,
deleteAppConnection
};
};

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 } 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 = "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 mintlify
// 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 GitHubAppConnectionSchema = 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 mintlify
// methods: z.tuple([z.literal(GitHubConnectionMethod.GitHubApp), 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,
GitHubAppConnectionSchema,
ValidateGitHubConnectionCredentialsSchema
} from "./github-connection-schemas";
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
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

@ -1,4 +1,5 @@
/* eslint-disable no-await-in-loop */
import opentelemetry from "@opentelemetry/api";
import { AxiosError } from "axios";
import {
@ -158,6 +159,12 @@ export const secretQueueFactory = ({
projectUserMembershipRoleDAL,
projectKeyDAL
}: 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(
@ -933,6 +940,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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/github/{connectionId}"
---
<Note>
GitHub Connections must be updated 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: "List"
openapi: "GET /api/v1/app-connections"
---

View File

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

116
docs/cli/commands/ssh.mdx Normal file
View File

@ -0,0 +1,116 @@
---
title: "infisical ssh"
description: "Generate SSH credentials with the CLI"
---
## Description
[Infisical SSH](/documentation/platform/ssh) lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure.
This command enables you to obtain SSH credentials used to access a remote host; we recommend using the `issue-credentials` sub-command to generate dynamic SSH credentials for each SSH session.
### Sub-commands
<Accordion title="infisical ssh issue-credentials">
This command is used to issue SSH credentials (SSH certificate, public key, and private key) against a certificate template.
We recommend using the `--addToAgent` flag to automatically load issued SSH credentials to the SSH agent.
```bash
$ infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<principals> --addToAgent
```
### Flags
<Accordion title="--certificateTemplateId">
The ID of the SSH certificate template to issue SSH credentials for.
</Accordion>
<Accordion title="--principals">
A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for.
</Accordion>
<Accordion title="--addToAgent">
Whether to add issued SSH credentials to the SSH agent.
Default value: `false`
Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--outFilePath">
The path to write the SSH credentials to such as `~/.ssh`, `./some_folder`, `./some_folder/id_rsa-cert.pub`. If not provided, the credentials will be saved to the current working directory where the command is run.
Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--keyAlgorithm">
The key algorithm to issue SSH credentials for.
Default value: `RSA_2048`
Available options: `RSA_2048`, `RSA_4096`, `EC_prime256v1`, `EC_secp384r1`.
</Accordion>
<Accordion title="--certType">
The certificate type to issue SSH credentials for.
Default value: `user`
Available options: `user` or `host`
</Accordion>
<Accordion title="--ttl">
The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`).
Defaults to the Default TTL value set in the certificate template.
</Accordion>
<Accordion title="--keyId">
A custom Key ID to issue SSH credentials for.
Defaults to the autogenerated Key ID by Infisical.
</Accordion>
<Accordion title="--token">
An authenticated token to use to issue SSH credentials.
</Accordion>
</Accordion>
<Accordion title="infisical ssh sign-key">
This command is used to sign an existing SSH public key against a certificate template; the command outputs the corresponding signed SSH certificate.
```bash
$ infisical ssh sign-key --certificateTemplateId=<certificate-template-id> --publicKey=<public-key> --principals=<principals> --outFilePath=<out-file-path>
```
<Accordion title="--certificateTemplateId">
The ID of the SSH certificate template to issue the SSH certificate for.
</Accordion>
<Accordion title="--publicKey">
The public key to sign.
Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--publicKeyFilePath">
The path to the public key file to sign.
Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--principals">
A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for.
</Accordion>
<Accordion title="--outFilePath">
The path to write the SSH certificate to such as `~/.ssh/id_rsa-cert.pub`; the specified file must have the `.pub` extension. If not provided, the credentials will be saved to the directory of the specified `--publicKeyFilePath` or the current working directory where the command is run.
</Accordion>
<Accordion title="--certType">
The certificate type to issue SSH credentials for.
Default value: `user`
Available options: `user` or `host`
</Accordion>
<Accordion title="--ttl">
The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`).
Defaults to the Default TTL value set in the certificate template.
</Accordion>
<Accordion title="--keyId">
A custom Key ID to issue SSH credentials for.
Defaults to the autogenerated Key ID by Infisical.
</Accordion>
<Accordion title="--token">
An authenticated token to use to issue SSH credentials.
</Accordion>
</Accordion>

View File

@ -4,6 +4,13 @@ sidebarTitle: "Overview"
description: "Learn how to generate secrets dynamically on-demand."
---
<Info>
Note that Dynamic Secrets is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**
If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Info>
## Introduction
Contrary to static key-value secrets, which require manual input of data into the secure Infisical storage, **dynamic secrets are generated on-demand upon access**.

View File

@ -3,6 +3,13 @@ title: "Approval Workflows"
description: "Learn how to enable a set of policies to manage changes to sensitive secrets and environments."
---
<Info>
Approval Workflows is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier** and **Enterprise Tire**.
If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Info>
## Problem at hand
Updating secrets in high-stakes environments (e.g., production) can have a number of problematic issues:
@ -40,4 +47,4 @@ When a user submits a change to an enviropnment that is under a particular polic
Approvers are notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)).
![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png)
![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png)

View File

@ -15,7 +15,7 @@ Prerequisites:
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
In Infisical, head to your Organization Settings > Security > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow Azure to provision/deprovision users for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)

View File

@ -15,7 +15,7 @@ Prerequisites:
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
In Infisical, head to your Organization Settings > Security > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users and user groups for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)

View File

@ -15,7 +15,7 @@ Prerequisites:
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
In Infisical, head to your Organization Settings > Security > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users and user groups for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)

View File

@ -6,7 +6,7 @@ description: "Learn how to generate SSH credentials to provide secure and centra
## Concept
Infisical can be used to issue SSH certificates to clients to provide short-lived, secure SSH access to infrastructure;
Infisical can be used to issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure;
this improves on many limitations of traditional SSH key-based authentication via mitigation of private key compromise, static key management,
unauthorized access, and SSH key sprawl.
@ -191,7 +191,9 @@ infisical login
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
For fuller documentation on commands and flags supported by the Infisical CLI for SSH, refer to the docs [here](/cli/commands/ssh).
</Step>
<Step title="SSH into the host">
Finally, SSH into the desired host; the SSH operation will be performed using the SSH certificate loaded into the SSH agent.
@ -199,11 +201,10 @@ infisical login
```bash
ssh username@hostname
```
</Step>
</Steps>
<Note>
Note that the above workflow can be executed via API or other client methods
such as SDK.
</Note>
</Note>

View File

@ -0,0 +1,93 @@
---
title: "Auth0 SAML"
description: "Learn how to configure Auth0 SAML for Infisical SSO."
---
<Info>
Auth0 SAML SSO feature is a paid feature. If you're using Infisical Cloud,
then it is available under the **Pro Tier**. If you're self-hosting Infisical,
then you should contact sales@infisical.com to purchase an enterprise license
to use it.
</Info>
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Auth0, then click **Connect** again.
Next, note the **Application Callback URL** and **Audience** to use when configuring the Auth0 SAML application.
![Auth0 SAML initial configuration](../../../images/sso/auth0-saml/init-config.png)
</Step>
<Step title="Create a SAML application in Auth0">
2.1. In your Auth0 account, head to Applications and create an application.
![Auth0 SAML app creation](../../../images/sso/auth0-saml/create-application.png)
Select **Regular Web Application** and press **Create**.
![Auth0 SAML app creation](../../../images/sso/auth0-saml/create-application-2.png)
2.2. In the Application head to Settings > Application URIs and add the **Application Callback URL** from step 1 into the **Allowed Callback URLs** field.
![Auth0 SAML allowed callback URLs](../../../images/sso/auth0-saml/auth0-config.png)
2.3. In the Application head to Addons > SAML2 Web App and copy the **Issuer**, **Identity Provider Login URL**, and **Identity Provider Certificate** from the **Usage** tab.
![Auth0 SAML config](../../../images/sso/auth0-saml/auth0-config-2.png)
2.4. Back in Infisical, set **Issuer**, **Identity Provider Login URL**, and **Certificate** to the corresponding items from step 2.3.
![Auth0 SAML Infisical config](../../../images/sso/auth0-saml/infisical-config.png)
2.5. Back in Auth0, in the **Settings** tab, set the **Application Callback URL** to the **Application Callback URL** from step 1
and update the **Settings** field with the JSON under the picture below (replacing `<audience-from-infisical>` with the **Audience** from step 1).
![Auth0 SAML config](../../../images/sso/auth0-saml/auth0-config-3.png)
```json
{
"audience": "<audience-from-infisical>",
"mappings": {
"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email",
"given_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName",
"family_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastName"
},
"signatureAlgorithm": "rsa-sha256",
"digestAlgorithm": "sha256",
"signResponse": true
}
```
Click **Save**.
</Step>
<Step title="Enable SAML SSO in Infisical">
Enabling SAML SSO allows members in your organization to log into Infisical via Auth0.
![Auth0 SAML enable](../../../images/sso/auth0-saml/enable-saml.png)
</Step>
<Step title="Enforce SAML SSO in Infisical">
Enforcing SAML SSO ensures that members in your organization can only access Infisical
by logging into the organization via Auth0.
To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Auth0 user with Infisical;
Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
</Step>
</Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite SAML login.
</Tip>
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work:
<div class="height:1px;"/>
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@ -12,7 +12,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Azure / Entra, then click **Connect** again.
Next, copy the **Reply URL (Assertion Consumer Service URL)** and **Identifier (Entity ID)** to use when configuring the Azure SAML application.

View File

@ -12,7 +12,7 @@ description: "Learn how to configure Google SAML for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Google, then click **Connect** again.
Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application.

View File

@ -12,7 +12,7 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select JumpCloud, then click **Connect** again.
Next, copy the **ACS URL** and **SP Entity ID** to use when configuring the JumpCloud SAML application.

View File

@ -12,7 +12,7 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Manage**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Keycloak, then click **Connect** again.
![Keycloak SAML organization security section](../../../images/sso/keycloak/org-security-section.png)

View File

@ -12,7 +12,7 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Okta, then click **Connect** again.
Next, copy the **Single sign-on URL** and **Audience URI (SP Entity ID)** to use when configuring the Okta SAML 2.0 application.
![Okta SAML initial configuration](../../../images/sso/okta/init-config.png)

View File

@ -28,6 +28,7 @@ Infisical supports these and many other identity providers:
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)
- [Keycloak SAML](/documentation/platform/sso/keycloak-saml)
- [Google SAML](/documentation/platform/sso/google-saml)
- [Auth0 SAML](/documentation/platform/sso/auth0-saml)
- [Keycloak OIDC](/documentation/platform/sso/keycloak-oidc)
- [Auth0 OIDC](/documentation/platform/sso/auth0-oidc)
- [General OIDC](/documentation/platform/sso/general-oidc)

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

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