Compare commits
117 Commits
cli-ssh-ag
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
b726187ba3 | |||
d98ff32b07 | |||
1fa510b32f | |||
c57f0d8120 | |||
00490f2cff | |||
ee58f538c0 | |||
0fa20f7839 | |||
40ef75d3bd | |||
26af13453c | |||
ad1f71883d | |||
2659ea7170 | |||
d2e3f504fd | |||
ca4151a34d | |||
d4bc104fd1 | |||
7e3a3fcdb0 | |||
7f67912d2f | |||
a7f4020c08 | |||
d2d89034ba | |||
2fc6c564c0 | |||
240b86c1d5 | |||
30d66cef89 | |||
b7d11444a9 | |||
0a6aef0afa | |||
0ac7ec460b | |||
808a901aee | |||
d5412f916f | |||
d0648ca596 | |||
3291f1f908 | |||
965d30bd03 | |||
68fe7c589a | |||
54377a44d3 | |||
8c902d7699 | |||
c25c84aeb3 | |||
4359eb7313 | |||
322536d738 | |||
6c5db3a187 | |||
a337e6badd | |||
524a97e9a6 | |||
c56f598115 | |||
19d32a1a3b | |||
7e5417a0eb | |||
afd6de27fe | |||
7781a6b7e7 | |||
b3b4e41d92 | |||
5225f5136a | |||
398adfaf76 | |||
d77c26fa38 | |||
ef7b81734a | |||
09b489a348 | |||
6b5c50def0 | |||
1f2d52176c | |||
7002e297c8 | |||
71864a131f | |||
9964d2ecaa | |||
3ebbaefc2a | |||
dd5c494bdb | |||
bace8af5a1 | |||
f56196b820 | |||
7042d73410 | |||
cb22ee315e | |||
701eb7cfc6 | |||
bf8df14b01 | |||
1ba8b6394b | |||
c442c8483a | |||
0435305a68 | |||
febf11f502 | |||
64fd15c32d | |||
a2c9494d52 | |||
18460e0678 | |||
3d03fece74 | |||
234e7eb9be | |||
04af313bf0 | |||
9b038ccc45 | |||
9beb384546 | |||
12ec9b4b4e | |||
96b8e7fda8 | |||
93b9108aa3 | |||
99017ea1ae | |||
f32588112e | |||
f9b0b6700a | |||
b45d9398f0 | |||
1d1140237f | |||
937560fd8d | |||
5f4b7b9ea7 | |||
05139820a5 | |||
7f6bc3ecfe | |||
d8cc000ad1 | |||
8fc03c06d9 | |||
50ceedf39f | |||
550096e72b | |||
1190ca2d77 | |||
2fb60201bc | |||
634b500244 | |||
54b4d4ae55 | |||
2f6dab3f63 | |||
e9564f5231 | |||
05cdca9202 | |||
5ab0c66dee | |||
2843818395 | |||
2357f3bc1f | |||
cde813aafb | |||
bbc8091d44 | |||
ce5e591457 | |||
5ae74f9761 | |||
eef331bbd1 | |||
d5c2e9236a | |||
13eef7e524 | |||
3fa84c578c | |||
c22ed04733 | |||
64fac1979f | |||
2d60f389c2 | |||
7798e5a2ad | |||
ed78227725 | |||
62968c5e43 | |||
7947e73569 | |||
52ce90846a | |||
242595fceb |
17
.env.example
@ -88,3 +88,20 @@ PLAIN_WISH_LABEL_IDS=
|
|||||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||||
|
|
||||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
||||||
|
|
||||||
|
# App Connections
|
||||||
|
|
||||||
|
# aws assume-role
|
||||||
|
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID=
|
||||||
|
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY=
|
||||||
|
|
||||||
|
# github oauth
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID=
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET=
|
||||||
|
|
||||||
|
#github app
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_ID=
|
4
.github/workflows/check-fe-ts-and-lint.yml
vendored
@ -18,10 +18,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 🔧 Setup Node 16
|
- name: 🔧 Setup Node 20
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
node-version: "20"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
- name: 📦 Install dependencies
|
- name: 📦 Install dependencies
|
||||||
|
@ -8,7 +8,7 @@ FROM node:20-slim AS base
|
|||||||
FROM base AS frontend-dependencies
|
FROM base AS frontend-dependencies
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci --only-production --ignore-scripts
|
RUN npm ci --only-production --ignore-scripts
|
||||||
@ -23,17 +23,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
|||||||
COPY /frontend .
|
COPY /frontend .
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV NEXT_PUBLIC_ENV production
|
|
||||||
ARG POSTHOG_HOST
|
ARG POSTHOG_HOST
|
||||||
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
ENV VITE_POSTHOG_HOST $POSTHOG_HOST
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||||
ARG INTERCOM_ID
|
ARG INTERCOM_ID
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
ENV VITE_INTERCOM_ID $INTERCOM_ID
|
||||||
ARG INFISICAL_PLATFORM_VERSION
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@ -44,20 +43,10 @@ WORKDIR /app
|
|||||||
|
|
||||||
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|
||||||
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
||||||
VOLUME /app/.next/cache/images
|
|
||||||
|
|
||||||
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
|
||||||
COPY --from=frontend-builder /app/public ./public
|
|
||||||
RUN chown non-root-user:nodejs ./public/data
|
|
||||||
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
|
||||||
|
|
||||||
USER non-root-user
|
USER non-root-user
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## BACKEND
|
## BACKEND
|
||||||
##
|
##
|
||||||
@ -137,7 +126,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
freetds-dev \
|
freetds-dev \
|
||||||
freetds-bin \
|
freetds-bin \
|
||||||
tdsodbc \
|
tdsodbc \
|
||||||
openssh \
|
openssh-client \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Configure ODBC in production
|
# Configure ODBC in production
|
||||||
@ -160,14 +149,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
|
|||||||
|
|
||||||
## set pre baked keys
|
## set pre baked keys
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
|
||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
ENV INTERCOM_ID=$INTERCOM_ID
|
||||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ RUN apk add --no-cache libc6-compat
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci --only-production --ignore-scripts
|
RUN npm ci --only-production --ignore-scripts
|
||||||
@ -27,17 +27,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
|||||||
COPY /frontend .
|
COPY /frontend .
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV NEXT_PUBLIC_ENV production
|
|
||||||
ARG POSTHOG_HOST
|
ARG POSTHOG_HOST
|
||||||
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
ENV VITE_POSTHOG_HOST $POSTHOG_HOST
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||||
ARG INTERCOM_ID
|
ARG INTERCOM_ID
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
ENV VITE_INTERCOM_ID $INTERCOM_ID
|
||||||
ARG INFISICAL_PLATFORM_VERSION
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@ -49,20 +48,10 @@ WORKDIR /app
|
|||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 non-root-user
|
RUN adduser --system --uid 1001 non-root-user
|
||||||
|
|
||||||
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
||||||
VOLUME /app/.next/cache/images
|
|
||||||
|
|
||||||
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
|
||||||
COPY --from=frontend-builder /app/public ./public
|
|
||||||
RUN chown non-root-user:nodejs ./public/data
|
|
||||||
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
|
||||||
|
|
||||||
USER non-root-user
|
USER non-root-user
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## BACKEND
|
## BACKEND
|
||||||
##
|
##
|
||||||
@ -159,14 +148,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
|
|||||||
|
|
||||||
## set pre baked keys
|
## set pre baked keys
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
|
||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
ENV INTERCOM_ID=$INTERCOM_ID
|
||||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
|
||||||
|
|
||||||
|
|
||||||
COPY --from=backend-runner /app /backend
|
COPY --from=backend-runner /app /backend
|
||||||
|
128
backend/package-lock.json
generated
@ -26,6 +26,7 @@
|
|||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/request-context": "^5.1.0",
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
|
"@fastify/static": "^7.0.4",
|
||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
"@google-cloud/kms": "^4.5.0",
|
"@google-cloud/kms": "^4.5.0",
|
||||||
@ -5406,6 +5407,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
|
||||||
"integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==",
|
"integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
@ -5545,6 +5547,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
|
||||||
"integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==",
|
"integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lukeed/ms": "^2.0.1",
|
"@lukeed/ms": "^2.0.1",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
@ -5563,16 +5566,85 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fastify/static": {
|
"node_modules/@fastify/static": {
|
||||||
"version": "6.12.0",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz",
|
||||||
"integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==",
|
"integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/accept-negotiator": "^1.0.0",
|
"@fastify/accept-negotiator": "^1.0.0",
|
||||||
"@fastify/send": "^2.0.0",
|
"@fastify/send": "^2.0.0",
|
||||||
"content-disposition": "^0.5.3",
|
"content-disposition": "^0.5.3",
|
||||||
"fastify-plugin": "^4.0.0",
|
"fastify-plugin": "^4.0.0",
|
||||||
"glob": "^8.0.1",
|
"fastq": "^1.17.0",
|
||||||
"p-limit": "^3.1.0"
|
"glob": "^10.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/glob": {
|
||||||
|
"version": "10.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
|
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.1.0",
|
||||||
|
"jackspeak": "^3.1.2",
|
||||||
|
"minimatch": "^9.0.4",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^1.11.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/jackspeak": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/cliui": "^8.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/minimatch": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static/node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fastify/swagger": {
|
"node_modules/@fastify/swagger": {
|
||||||
@ -5599,6 +5671,20 @@
|
|||||||
"yaml": "^2.2.2"
|
"yaml": "^2.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/swagger-ui/node_modules/@fastify/static": {
|
||||||
|
"version": "6.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz",
|
||||||
|
"integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/accept-negotiator": "^1.0.0",
|
||||||
|
"@fastify/send": "^2.0.0",
|
||||||
|
"content-disposition": "^0.5.3",
|
||||||
|
"fastify-plugin": "^4.0.0",
|
||||||
|
"glob": "^8.0.1",
|
||||||
|
"p-limit": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@google-cloud/kms": {
|
"node_modules/@google-cloud/kms": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
|
||||||
@ -6062,9 +6148,10 @@
|
|||||||
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
|
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@lukeed/ms": {
|
"node_modules/@lukeed/ms": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
|
||||||
"integrity": "sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==",
|
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -13879,9 +13966,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.1",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
@ -13903,7 +13990,7 @@
|
|||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.10",
|
"path-to-regexp": "0.1.12",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.13.0",
|
"qs": "6.13.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
@ -13918,6 +14005,10 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-session": {
|
"node_modules/express-session": {
|
||||||
@ -17388,15 +17479,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.cjs"
|
||||||
},
|
},
|
||||||
@ -18383,9 +18475,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.10",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
|
@ -134,6 +134,7 @@
|
|||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/request-context": "^5.1.0",
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
|
"@fastify/static": "^7.0.4",
|
||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
"@google-cloud/kms": "^4.5.0",
|
"@google-cloud/kms": "^4.5.0",
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
@ -36,6 +36,7 @@ import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-cert
|
|||||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||||
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
@ -208,6 +209,7 @@ declare module "fastify" {
|
|||||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||||
projectTemplate: TProjectTemplateServiceFactory;
|
projectTemplate: TProjectTemplateServiceFactory;
|
||||||
totp: TTotpServiceFactory;
|
totp: TTotpServiceFactory;
|
||||||
|
appConnection: TAppConnectionServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
6
backend/src/@types/knex.d.ts
vendored
@ -363,6 +363,7 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections";
|
||||||
import {
|
import {
|
||||||
TExternalGroupOrgRoleMappings,
|
TExternalGroupOrgRoleMappings,
|
||||||
TExternalGroupOrgRoleMappingsInsert,
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
@ -886,5 +887,10 @@ declare module "knex/types/tables" {
|
|||||||
TProjectSplitBackfillIdsInsert,
|
TProjectSplitBackfillIdsInsert,
|
||||||
TProjectSplitBackfillIdsUpdate
|
TProjectSplitBackfillIdsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
|
||||||
|
TAppConnections,
|
||||||
|
TAppConnectionsInsert,
|
||||||
|
TAppConnectionsUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
backend/src/db/migrations/20241218181018_app-connection.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.AppConnection))) {
|
||||||
|
await knex.schema.createTable(TableName.AppConnection, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name", 32).notNullable();
|
||||||
|
t.string("description");
|
||||||
|
t.string("app").notNullable();
|
||||||
|
t.string("method").notNullable();
|
||||||
|
t.binary("encryptedCredentials").notNullable();
|
||||||
|
t.integer("version").defaultTo(1).notNullable();
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.AppConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.AppConnection);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.AppConnection);
|
||||||
|
}
|
27
backend/src/db/schemas/app-connections.ts
Normal file
@ -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>>;
|
@ -129,7 +129,8 @@ export enum TableName {
|
|||||||
KmsKeyVersion = "kms_key_versions",
|
KmsKeyVersion = "kms_key_versions",
|
||||||
WorkflowIntegrations = "workflow_integrations",
|
WorkflowIntegrations = "workflow_integrations",
|
||||||
SlackIntegrations = "slack_integrations",
|
SlackIntegrations = "slack_integrations",
|
||||||
ProjectSlackConfigs = "project_slack_configs"
|
ProjectSlackConfigs = "project_slack_configs",
|
||||||
|
AppConnection = "app_connections"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
),
|
),
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().nullish(),
|
||||||
permissions: z.any().array()
|
permissions: z.any().array()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -95,7 +95,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().nullish(),
|
||||||
permissions: z.any().array().optional()
|
permissions: z.any().array().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@ -39,7 +39,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -95,7 +95,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||||
.optional(),
|
.optional(),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@ -36,7 +36,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -91,7 +91,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@ -213,7 +213,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||||
const approvalUrl = `${cfg.SITE_URL}/project/${project.id}/approval`;
|
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
|
||||||
|
|
||||||
await triggerSlackNotification({
|
await triggerSlackNotification({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
@ -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 { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
@ -222,7 +224,12 @@ export enum EventType {
|
|||||||
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
||||||
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
||||||
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
||||||
APPLY_PROJECT_TEMPLATE = "apply-project-template"
|
APPLY_PROJECT_TEMPLATE = "apply-project-template",
|
||||||
|
GET_APP_CONNECTIONS = "get-app-connections",
|
||||||
|
GET_APP_CONNECTION = "get-app-connection",
|
||||||
|
CREATE_APP_CONNECTION = "create-app-connection",
|
||||||
|
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||||
|
DELETE_APP_CONNECTION = "delete-app-connection"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@ -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 =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@ -2038,4 +2078,9 @@ export type Event =
|
|||||||
| CreateProjectTemplateEvent
|
| CreateProjectTemplateEvent
|
||||||
| UpdateProjectTemplateEvent
|
| UpdateProjectTemplateEvent
|
||||||
| DeleteProjectTemplateEvent
|
| DeleteProjectTemplateEvent
|
||||||
| ApplyProjectTemplateEvent;
|
| ApplyProjectTemplateEvent
|
||||||
|
| GetAppConnectionsEvent
|
||||||
|
| GetAppConnectionEvent
|
||||||
|
| CreateAppConnectionEvent
|
||||||
|
| UpdateAppConnectionEvent
|
||||||
|
| DeleteAppConnectionEvent;
|
||||||
|
@ -49,7 +49,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
},
|
},
|
||||||
pkiEst: false,
|
pkiEst: false,
|
||||||
enforceMfa: false,
|
enforceMfa: false,
|
||||||
projectTemplates: false
|
projectTemplates: false,
|
||||||
|
appConnections: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@ -67,6 +67,7 @@ export type TFeatureSet = {
|
|||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
projectTemplates: false;
|
projectTemplates: false;
|
||||||
|
appConnections: false; // TODO: remove once live
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@ -27,7 +27,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console",
|
AdminConsole = "organization-admin-console",
|
||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
ProjectTemplates = "project-templates"
|
ProjectTemplates = "project-templates",
|
||||||
|
AppConnections = "app-connections"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@ -46,6 +47,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
@ -123,6 +125,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
@ -153,6 +160,8 @@ const buildMemberPermission = () => {
|
|||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ export const sendApprovalEmailsFn = async ({
|
|||||||
firstName: reviewerUser.firstName,
|
firstName: reviewerUser.firstName,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
organizationName: project.organization.name,
|
organizationName: project.organization.name,
|
||||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.SecretApprovalRequestNeedsReview
|
template: SmtpTemplates.SecretApprovalRequestNeedsReview
|
||||||
});
|
});
|
||||||
|
@ -852,7 +852,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
bypassReason,
|
bypassReason,
|
||||||
secretPath: policy.secretPath,
|
secretPath: policy.secretPath,
|
||||||
environment: env.name,
|
environment: env.name,
|
||||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.AccessSecretRequestBypassed
|
template: SmtpTemplates.AccessSecretRequestBypassed
|
||||||
});
|
});
|
||||||
|
@ -5,6 +5,7 @@ import { ProjectType } from "@app/db/schemas";
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
||||||
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
||||||
import {
|
import {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
|
||||||
export const GROUPS = {
|
export const GROUPS = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
name: "The name of the group to create.",
|
name: "The name of the group to create.",
|
||||||
@ -1605,3 +1608,34 @@ export const ProjectTemplates = {
|
|||||||
templateId: "The ID of the project template to be deleted."
|
templateId: "The ID of the project template to be deleted."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AppConnections = {
|
||||||
|
GET_BY_ID: (app: AppConnection) => ({
|
||||||
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||||
|
}),
|
||||||
|
GET_BY_NAME: (app: AppConnection) => ({
|
||||||
|
connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||||
|
}),
|
||||||
|
CREATE: (app: AppConnection) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
return {
|
||||||
|
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||||
|
description: `An optional description for the ${appName} Connection.`,
|
||||||
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
|
method: `The method used to authenticate with ${appName}.`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
UPDATE: (app: AppConnection) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
return {
|
||||||
|
connectionId: `The ID of the ${appName} Connection to be updated.`,
|
||||||
|
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||||
|
description: `The updated description of the ${appName} Connection.`,
|
||||||
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
|
method: `The method used to authenticate with ${appName}.`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
DELETE: (app: AppConnection) => ({
|
||||||
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} connection to be deleted.`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
@ -157,6 +157,8 @@ const envSchema = z
|
|||||||
INFISICAL_CLOUD: zodStrBool.default("false"),
|
INFISICAL_CLOUD: zodStrBool.default("false"),
|
||||||
MAINTENANCE_MODE: zodStrBool.default("false"),
|
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||||
|
CAPTCHA_SITE_KEY: zpStr(z.string().optional()),
|
||||||
|
INTERCOM_ID: zpStr(z.string().optional()),
|
||||||
|
|
||||||
// TELEMETRY
|
// TELEMETRY
|
||||||
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
|
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
|
||||||
@ -180,7 +182,24 @@ const envSchema = z
|
|||||||
HSM_SLOT: z.coerce.number().optional().default(0),
|
HSM_SLOT: z.coerce.number().optional().default(0),
|
||||||
|
|
||||||
USE_PG_QUEUE: zodStrBool.default("false"),
|
USE_PG_QUEUE: zodStrBool.default("false"),
|
||||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false")
|
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
||||||
|
|
||||||
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// aws
|
||||||
|
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// github oauth
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// github app
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional())
|
||||||
})
|
})
|
||||||
// To ensure that basic encryption is always possible.
|
// To ensure that basic encryption is always possible.
|
||||||
.refine(
|
.refine(
|
||||||
|
@ -14,3 +14,5 @@ export const prefixWithSlash = (str: string) => {
|
|||||||
if (str.startsWith("/")) return str;
|
if (str.startsWith("/")) return str;
|
||||||
return `/${str}`;
|
return `/${str}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
|
||||||
|
@ -43,6 +43,8 @@ export type RequiredKeys<T> = {
|
|||||||
|
|
||||||
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
||||||
|
|
||||||
|
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
||||||
|
|
||||||
export enum EnforcementLevel {
|
export enum EnforcementLevel {
|
||||||
Hard = "hard",
|
Hard = "hard",
|
||||||
Soft = "soft"
|
Soft = "soft"
|
||||||
|
@ -27,10 +27,10 @@ import { globalRateLimiterCfg } from "./config/rateLimiter";
|
|||||||
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
|
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
|
||||||
import { apiMetrics } from "./plugins/api-metrics";
|
import { apiMetrics } from "./plugins/api-metrics";
|
||||||
import { fastifyErrHandler } from "./plugins/error-handler";
|
import { fastifyErrHandler } from "./plugins/error-handler";
|
||||||
import { registerExternalNextjs } from "./plugins/external-nextjs";
|
|
||||||
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
||||||
import { fastifyIp } from "./plugins/ip";
|
import { fastifyIp } from "./plugins/ip";
|
||||||
import { maintenanceMode } from "./plugins/maintenanceMode";
|
import { maintenanceMode } from "./plugins/maintenanceMode";
|
||||||
|
import { registerServeUI } from "./plugins/serve-ui";
|
||||||
import { fastifySwagger } from "./plugins/swagger";
|
import { fastifySwagger } from "./plugins/swagger";
|
||||||
import { registerRoutes } from "./routes";
|
import { registerRoutes } from "./routes";
|
||||||
|
|
||||||
@ -120,13 +120,10 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
|
|||||||
|
|
||||||
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
|
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
|
||||||
|
|
||||||
if (appCfg.isProductionMode) {
|
await server.register(registerServeUI, {
|
||||||
await server.register(registerExternalNextjs, {
|
|
||||||
standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED,
|
standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED,
|
||||||
dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../"),
|
dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../")
|
||||||
port: appCfg.PORT
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
await server.ready();
|
await server.ready();
|
||||||
server.swagger();
|
server.swagger();
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ForbiddenError, PureAbility } from "@casl/ability";
|
import { ForbiddenError, PureAbility } from "@casl/ability";
|
||||||
|
import opentelemetry from "@opentelemetry/api";
|
||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import {
|
import {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
@ -35,8 +37,30 @@ enum HttpStatusCodes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const apiMeter = opentelemetry.metrics.getMeter("API");
|
||||||
|
const errorHistogram = apiMeter.createHistogram("API_errors", {
|
||||||
|
description: "API errors by type, status code, and name",
|
||||||
|
unit: "1"
|
||||||
|
});
|
||||||
|
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
const { method } = req;
|
||||||
|
const route = req.routerPath;
|
||||||
|
const errorType =
|
||||||
|
error instanceof jwt.JsonWebTokenError ? "TokenError" : error.constructor.name || "UnknownError";
|
||||||
|
|
||||||
|
errorHistogram.record(1, {
|
||||||
|
route,
|
||||||
|
method,
|
||||||
|
type: errorType,
|
||||||
|
name: error.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
void res
|
void res
|
||||||
.status(HttpStatusCodes.BadRequest)
|
.status(HttpStatusCodes.BadRequest)
|
||||||
@ -52,13 +76,20 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
message: error.message,
|
message: error.message,
|
||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
} else if (error instanceof DatabaseError) {
|
||||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
statusCode: HttpStatusCodes.InternalServerError,
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
message: "Something went wrong",
|
message: "Something went wrong",
|
||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
|
} else if (error instanceof InternalServerError) {
|
||||||
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
|
reqId: req.id,
|
||||||
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
|
message: error.message ?? "Something went wrong",
|
||||||
|
error: error.name
|
||||||
|
});
|
||||||
} else if (error instanceof GatewayTimeoutError) {
|
} else if (error instanceof GatewayTimeoutError) {
|
||||||
void res.status(HttpStatusCodes.GatewayTimeout).send({
|
void res.status(HttpStatusCodes.GatewayTimeout).send({
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
// this plugins allows to run infisical in standalone mode
|
|
||||||
// standalone mode = infisical backend and nextjs frontend in one server
|
|
||||||
// this way users don't need to deploy two things
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import { IS_PACKAGED } from "@app/lib/config/env";
|
|
||||||
|
|
||||||
// to enabled this u need to set standalone mode to true
|
|
||||||
export const registerExternalNextjs = async (
|
|
||||||
server: FastifyZodProvider,
|
|
||||||
{
|
|
||||||
standaloneMode,
|
|
||||||
dir,
|
|
||||||
port
|
|
||||||
}: {
|
|
||||||
standaloneMode?: boolean;
|
|
||||||
dir: string;
|
|
||||||
port: number;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
if (standaloneMode) {
|
|
||||||
const frontendName = IS_PACKAGED ? "frontend" : "frontend-build";
|
|
||||||
const nextJsBuildPath = path.join(dir, frontendName);
|
|
||||||
|
|
||||||
const { default: conf } = (await import(
|
|
||||||
path.join(dir, `${frontendName}/.next/required-server-files.json`),
|
|
||||||
// @ts-expect-error type
|
|
||||||
{
|
|
||||||
assert: { type: "json" }
|
|
||||||
}
|
|
||||||
)) as { default: { config: string } };
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let NextServer: any;
|
|
||||||
|
|
||||||
if (!IS_PACKAGED) {
|
|
||||||
/* eslint-disable */
|
|
||||||
const { default: nextServer } = (
|
|
||||||
await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`))
|
|
||||||
).default;
|
|
||||||
|
|
||||||
NextServer = nextServer;
|
|
||||||
} else {
|
|
||||||
/* eslint-disable */
|
|
||||||
const nextServer = await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`));
|
|
||||||
|
|
||||||
NextServer = nextServer.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextApp = new NextServer({
|
|
||||||
dev: false,
|
|
||||||
dir: nextJsBuildPath,
|
|
||||||
port,
|
|
||||||
conf: conf.config,
|
|
||||||
hostname: "local",
|
|
||||||
customServer: false
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: ["GET", "PUT", "PATCH", "POST", "DELETE"],
|
|
||||||
url: "/*",
|
|
||||||
schema: {
|
|
||||||
hide: true
|
|
||||||
},
|
|
||||||
handler: (req, res) =>
|
|
||||||
nextApp
|
|
||||||
.getRequestHandler()(req.raw, res.raw)
|
|
||||||
.then(() => {
|
|
||||||
res.hijack();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
server.addHook("onClose", () => nextApp.close());
|
|
||||||
await nextApp.prepare();
|
|
||||||
/* eslint-enable */
|
|
||||||
}
|
|
||||||
};
|
|
54
backend/src/server/plugins/serve-ui.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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",
|
||||||
|
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.get("/*", (request, reply) => {
|
||||||
|
if (request.url.startsWith("/api")) {
|
||||||
|
reply.callNotFound();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void reply.sendFile("index.html");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -91,6 +91,8 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||||
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
|
import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||||
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||||
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||||
@ -314,6 +316,7 @@ export const registerRoutes = async (
|
|||||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
const telemetryDAL = telemetryDALFactory(db);
|
const telemetryDAL = telemetryDALFactory(db);
|
||||||
|
const appConnectionDAL = appConnectionDALFactory(db);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDAL = permissionDALFactory(db);
|
const permissionDAL = permissionDALFactory(db);
|
||||||
@ -1352,6 +1355,13 @@ export const registerRoutes = async (
|
|||||||
externalGroupOrgRoleMappingDAL
|
externalGroupOrgRoleMappingDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appConnectionService = appConnectionServiceFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@ -1448,7 +1458,8 @@ export const registerRoutes = async (
|
|||||||
migration: migrationService,
|
migration: migrationService,
|
||||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
||||||
projectTemplate: projectTemplateService,
|
projectTemplate: projectTemplateService,
|
||||||
totp: totpService
|
totp: totpService,
|
||||||
|
appConnection: appConnectionService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||||
|
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
// can't use discriminated due to multiple schemas for certain apps
|
||||||
|
const SanitizedAppConnectionSchema = z.union([
|
||||||
|
...SanitizedAwsConnectionSchema.options,
|
||||||
|
...SanitizedGitHubConnectionSchema.options
|
||||||
|
]);
|
||||||
|
|
||||||
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
|
AwsConnectionListItemSchema,
|
||||||
|
GitHubConnectionListItemSchema
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/options",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List the available App Connection Options.",
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
appConnectionOptions: AppConnectionOptionsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: () => {
|
||||||
|
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions();
|
||||||
|
return { appConnectionOptions };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List all the App Connections for the current organization.",
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTIONS,
|
||||||
|
metadata: {
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnections };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,274 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { startsWithVowel } from "@app/lib/fn";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
|
||||||
|
server,
|
||||||
|
app,
|
||||||
|
createSchema,
|
||||||
|
updateSchema,
|
||||||
|
responseSchema
|
||||||
|
}: {
|
||||||
|
app: AppConnection;
|
||||||
|
server: FastifyZodProvider;
|
||||||
|
createSchema: z.ZodType<{
|
||||||
|
name: string;
|
||||||
|
method: I["method"];
|
||||||
|
credentials: I["credentials"];
|
||||||
|
description?: string | null;
|
||||||
|
}>;
|
||||||
|
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
||||||
|
responseSchema: z.ZodTypeAny;
|
||||||
|
}) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `List the ${appName} Connections for the current organization.`,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnections: responseSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[];
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTIONS,
|
||||||
|
metadata: {
|
||||||
|
app,
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnections };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:connectionId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${appName} Connection by ID.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.findAppConnectionById(
|
||||||
|
app,
|
||||||
|
connectionId,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/name/:connectionName`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${appName} Connection by name.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionName: z
|
||||||
|
.string()
|
||||||
|
.min(0, "Connection name required")
|
||||||
|
.describe(AppConnections.GET_BY_NAME(app).connectionName)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionName } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.findAppConnectionByName(
|
||||||
|
app,
|
||||||
|
connectionName,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId: appConnection.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Create ${
|
||||||
|
startsWithVowel(appName) ? "an" : "a"
|
||||||
|
} ${appName} Connection for the current organization.`,
|
||||||
|
body: createSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { name, method, credentials, description } = req.body;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||||
|
{ name, method, app, credentials, description },
|
||||||
|
req.permission
|
||||||
|
)) as TAppConnection;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
method,
|
||||||
|
app,
|
||||||
|
connectionId: appConnection.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:connectionId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Update the specified ${appName} Connection.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.UPDATE(app).connectionId)
|
||||||
|
}),
|
||||||
|
body: updateSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { name, credentials, description } = req.body;
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.updateAppConnection(
|
||||||
|
{ name, credentials, connectionId, description },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
credentialsUpdated: Boolean(credentials),
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/:connectionId`,
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Delete the specified ${appName} Connection.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.deleteAppConnection(
|
||||||
|
app,
|
||||||
|
connectionId,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,17 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateAwsConnectionSchema,
|
||||||
|
SanitizedAwsConnectionSchema,
|
||||||
|
UpdateAwsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.AWS,
|
||||||
|
server,
|
||||||
|
responseSchema: SanitizedAwsConnectionSchema,
|
||||||
|
createSchema: CreateAwsConnectionSchema,
|
||||||
|
updateSchema: UpdateAwsConnectionSchema
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateGitHubConnectionSchema,
|
||||||
|
SanitizedGitHubConnectionSchema,
|
||||||
|
UpdateGitHubConnectionSchema
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.GitHub,
|
||||||
|
server,
|
||||||
|
responseSchema: SanitizedGitHubConnectionSchema,
|
||||||
|
createSchema: CreateGitHubConnectionSchema,
|
||||||
|
updateSchema: UpdateGitHubConnectionSchema
|
||||||
|
});
|
@ -0,0 +1,8 @@
|
|||||||
|
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
|
||||||
|
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
|
||||||
|
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||||
|
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./app-connection-router";
|
||||||
|
export * from "./apps";
|
@ -63,7 +63,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
token: z.string()
|
token: z.string(),
|
||||||
|
organizationId: z.string().optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -115,7 +116,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { token };
|
return { token, organizationId: decodedToken.organizationId };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { APP_CONNECTION_REGISTER_MAP, registerAppConnectionRouter } from "@app/server/routes/v1/app-connection-routers";
|
||||||
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
||||||
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
||||||
|
|
||||||
@ -110,4 +111,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
await server.register(registerCmekRouter, { prefix: "/kms" });
|
||||||
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (appConnectionsRouter) => {
|
||||||
|
await appConnectionsRouter.register(registerAppConnectionRouter);
|
||||||
|
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_MAP)) {
|
||||||
|
await appConnectionsRouter.register(router, { prefix: `/${app}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prefix: "/app-connections" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -331,12 +331,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
failureAsync: async () => {
|
failureAsync: async () => {
|
||||||
return res.redirect(appCfg.SITE_URL as string);
|
return res.redirect(appCfg.SITE_URL as string);
|
||||||
},
|
},
|
||||||
successAsync: async (installation) => {
|
successAsync: async () => {
|
||||||
const metadata = JSON.parse(installation.metadata || "") as {
|
return res.redirect(`${appCfg.SITE_URL}/organization/settings?selectedTab=workflow-integrations`);
|
||||||
orgId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.redirect(`${appCfg.SITE_URL}/org/${metadata.orgId}/settings?selectedTab=workflow-integrations`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
11
backend/src/services/app-connection/app-connection-dal.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
|
||||||
|
|
||||||
|
export const appConnectionDALFactory = (db: TDbClient) => {
|
||||||
|
const appConnectionOrm = ormify(db, TableName.AppConnection);
|
||||||
|
|
||||||
|
return { ...appConnectionOrm };
|
||||||
|
};
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum AppConnection {
|
||||||
|
GitHub = "github",
|
||||||
|
AWS = "aws"
|
||||||
|
}
|
92
backend/src/services/app-connection/app-connection-fns.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||||
|
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||||
|
import {
|
||||||
|
AwsConnectionMethod,
|
||||||
|
getAwsAppConnectionListItem,
|
||||||
|
validateAwsConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
getGitHubConnectionListItem,
|
||||||
|
GitHubConnectionMethod,
|
||||||
|
validateGitHubConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
export const listAppConnectionOptions = () => {
|
||||||
|
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptAppConnectionCredentials = async ({
|
||||||
|
orgId,
|
||||||
|
credentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
credentials: TAppConnection["credentials"];
|
||||||
|
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||||
|
}) => {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||||
|
plainText: Buffer.from(JSON.stringify(credentials))
|
||||||
|
});
|
||||||
|
|
||||||
|
return encryptedCredentialsBlob;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptAppConnectionCredentials = async ({
|
||||||
|
orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
encryptedCredentials: Buffer;
|
||||||
|
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||||
|
}) => {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPlainTextBlob = decryptor({
|
||||||
|
cipherTextBlob: encryptedCredentials
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAppConnectionCredentials = async (
|
||||||
|
appConnection: TAppConnectionConfig
|
||||||
|
): Promise<TAppConnection["credentials"]> => {
|
||||||
|
const { app } = appConnection;
|
||||||
|
switch (app) {
|
||||||
|
case AppConnection.AWS: {
|
||||||
|
return validateAwsConnectionCredentials(appConnection);
|
||||||
|
}
|
||||||
|
case AppConnection.GitHub:
|
||||||
|
return validateGitHubConnectionCredentials(appConnection);
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled App Connection ${app}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||||
|
switch (method) {
|
||||||
|
case GitHubConnectionMethod.App:
|
||||||
|
return "GitHub App";
|
||||||
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
return "OAuth";
|
||||||
|
case AwsConnectionMethod.AccessKey:
|
||||||
|
return "Access Key";
|
||||||
|
case AwsConnectionMethod.AssumeRole:
|
||||||
|
return "Assume Role";
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { AppConnection } from "./app-connection-enums";
|
||||||
|
|
||||||
|
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||||
|
[AppConnection.AWS]: "AWS",
|
||||||
|
[AppConnection.GitHub]: "GitHub"
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
|
||||||
|
import { AppConnection } from "./app-connection-enums";
|
||||||
|
|
||||||
|
export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||||
|
encryptedCredentials: true,
|
||||||
|
app: true,
|
||||||
|
method: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(AppConnections.CREATE(app).description)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(AppConnections.UPDATE(app).description)
|
||||||
|
});
|
360
backend/src/services/app-connection/app-connection-service.ts
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
decryptAppConnectionCredentials,
|
||||||
|
encryptAppConnectionCredentials,
|
||||||
|
getAppConnectionMethodName,
|
||||||
|
listAppConnectionOptions,
|
||||||
|
validateAppConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import {
|
||||||
|
TAppConnection,
|
||||||
|
TAppConnectionConfig,
|
||||||
|
TCreateAppConnectionDTO,
|
||||||
|
TUpdateAppConnectionDTO,
|
||||||
|
TValidateAppConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
||||||
|
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
|
|
||||||
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">; // TODO: remove once launched
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||||
|
|
||||||
|
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||||
|
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||||
|
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appConnectionServiceFactory = ({
|
||||||
|
appConnectionDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
licenseService
|
||||||
|
}: TAppConnectionServiceFactoryDep) => {
|
||||||
|
// app connections are disabled for public until launch
|
||||||
|
const checkAppServicesAvailability = async (orgId: string) => {
|
||||||
|
const subscription = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
|
if (!subscription.appConnections) throw new BadRequestError({ message: "App Connections are not available yet." });
|
||||||
|
};
|
||||||
|
|
||||||
|
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const appConnections = await appConnectionDAL.find(
|
||||||
|
app
|
||||||
|
? { orgId: actor.orgId, app }
|
||||||
|
: {
|
||||||
|
orgId: actor.orgId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
appConnections
|
||||||
|
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
||||||
|
.map(async ({ encryptedCredentials, ...connection }) => {
|
||||||
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService,
|
||||||
|
orgId: connection.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...connection,
|
||||||
|
credentials
|
||||||
|
} as TAppConnection;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAppConnectionById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...appConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId });
|
||||||
|
|
||||||
|
if (!appConnection)
|
||||||
|
throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...appConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAppConnection = async (
|
||||||
|
{ method, app, credentials, ...params }: TCreateAppConnectionDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await appConnectionDAL.findOne(
|
||||||
|
{
|
||||||
|
name: params.name,
|
||||||
|
orgId: actor.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `An App Connection with the name "${params.name}" already exists`
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
credentials,
|
||||||
|
method,
|
||||||
|
orgId: actor.orgId
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = await appConnectionDAL.create(
|
||||||
|
{
|
||||||
|
orgId: actor.orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
method,
|
||||||
|
app,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...connection,
|
||||||
|
credentials: validatedCredentials
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return appConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAppConnection = async (
|
||||||
|
{ connectionId, credentials, ...params }: TUpdateAppConnectionDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
|
if (params.name && appConnection.name !== params.name) {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await appConnectionDAL.findOne(
|
||||||
|
{
|
||||||
|
name: params.name,
|
||||||
|
orgId: appConnection.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `An App Connection with the name "${params.name}" already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let encryptedCredentials: undefined | Buffer;
|
||||||
|
|
||||||
|
if (credentials) {
|
||||||
|
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
||||||
|
method,
|
||||||
|
credentials
|
||||||
|
}).success
|
||||||
|
)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid credential format for ${
|
||||||
|
APP_CONNECTION_NAME_MAP[app]
|
||||||
|
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
credentials,
|
||||||
|
method
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
if (!validatedCredentials)
|
||||||
|
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||||
|
|
||||||
|
encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedConnection = await appConnectionDAL.updateById(
|
||||||
|
connectionId,
|
||||||
|
{
|
||||||
|
orgId: actor.orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedConnection;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedAppConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: updatedAppConnection.encryptedCredentials,
|
||||||
|
orgId: updatedAppConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||||
|
|
||||||
|
// TODO: specify delete error message if due to existing dependencies
|
||||||
|
|
||||||
|
const deletedAppConnection = await appConnectionDAL.deleteById(connectionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...deletedAppConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: deletedAppConnection.encryptedCredentials,
|
||||||
|
orgId: deletedAppConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listAppConnectionOptions,
|
||||||
|
listAppConnectionsByOrg,
|
||||||
|
findAppConnectionById,
|
||||||
|
findAppConnectionByName,
|
||||||
|
createAppConnection,
|
||||||
|
updateAppConnection,
|
||||||
|
deleteAppConnection
|
||||||
|
};
|
||||||
|
};
|
31
backend/src/services/app-connection/app-connection-types.ts
Normal file
@ -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;
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum AwsConnectionMethod {
|
||||||
|
AssumeRole = "assume-role",
|
||||||
|
AccessKey = "access-key"
|
||||||
|
}
|
105
backend/src/services/app-connection/aws/aws-connection-fns.ts
Normal 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;
|
||||||
|
};
|
@ -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()
|
||||||
|
});
|
@ -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;
|
||||||
|
};
|
4
backend/src/services/app-connection/aws/index.ts
Normal 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";
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum GitHubConnectionMethod {
|
||||||
|
OAuth = "oauth",
|
||||||
|
App = "github-app"
|
||||||
|
}
|
@ -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}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -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()
|
||||||
|
});
|
@ -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">;
|
4
backend/src/services/app-connection/github/index.ts
Normal 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";
|
@ -305,10 +305,16 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
value: string;
|
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[] = [];
|
let result: AzureAppConfigKeyValue[] = [];
|
||||||
while (url) {
|
while (url) {
|
||||||
const res = await request.get(url, {
|
const res = await request.get(url, {
|
||||||
|
baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
},
|
},
|
||||||
@ -319,7 +325,7 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
result = result.concat(res.data.items);
|
result = result.concat(res.data.items);
|
||||||
url = res.data.nextLink;
|
url = res.data?.["@nextLink"];
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -327,11 +333,13 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
|
|
||||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||||
|
|
||||||
const azureAppConfigValuesUrl = `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
|
const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
|
||||||
metadata.azureLabel ? `&label=${metadata.azureLabel}` : ""
|
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) => {
|
||||||
accum[entry.key] = entry.value;
|
accum[entry.key] = entry.value;
|
||||||
|
|
||||||
@ -1159,7 +1167,8 @@ const syncSecretsAWSSecretManager = async ({
|
|||||||
} else {
|
} else {
|
||||||
await secretsManager.send(
|
await secretsManager.send(
|
||||||
new DeleteSecretCommand({
|
new DeleteSecretCommand({
|
||||||
SecretId: secretId
|
SecretId: secretId,
|
||||||
|
ForceDeleteWithoutRecovery: true
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1397,14 +1406,24 @@ const syncSecretsHeroku = async ({
|
|||||||
* Sync/push [secrets] to Vercel project named [integration.app]
|
* Sync/push [secrets] to Vercel project named [integration.app]
|
||||||
*/
|
*/
|
||||||
const syncSecretsVercel = async ({
|
const syncSecretsVercel = async ({
|
||||||
|
createManySecretsRawFn,
|
||||||
integration,
|
integration,
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
secrets,
|
secrets: infisicalSecrets,
|
||||||
accessToken
|
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;
|
integrationAuth: TIntegrationAuths;
|
||||||
secrets: Record<string, { value: string; comment?: string }>;
|
secrets: Record<string, { value: string; comment?: string } | null>;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
interface VercelSecret {
|
interface VercelSecret {
|
||||||
@ -1477,17 +1496,68 @@ const syncSecretsVercel = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSecrets: VercelSecret[] = [];
|
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||||
const deleteSecrets: VercelSecret[] = [];
|
|
||||||
const newSecrets: VercelSecret[] = [];
|
|
||||||
|
|
||||||
// Identify secrets to create
|
// Default to overwrite target for old integrations that doesn't have a initial sync behavior set.
|
||||||
Object.keys(secrets).forEach((key) => {
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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: ""
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)) {
|
if (!(key in res)) {
|
||||||
// case: secret has been created
|
await request.post(
|
||||||
newSecrets.push({
|
`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||||
|
{
|
||||||
key,
|
key,
|
||||||
value: secrets[key].value,
|
value: infisicalSecrets[key]?.value,
|
||||||
type: "encrypted",
|
type: "encrypted",
|
||||||
target: [integration.targetEnvironment as string],
|
target: [integration.targetEnvironment as string],
|
||||||
...(integration.path
|
...(integration.path
|
||||||
@ -1495,19 +1565,23 @@ const syncSecretsVercel = async ({
|
|||||||
gitBranch: integration.path
|
gitBranch: integration.path
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Identify secrets to update and delete
|
// Else if the key already exists and its not sensitive, we need to update it
|
||||||
Object.keys(res).forEach((key) => {
|
} else if (res[key].type !== "sensitive") {
|
||||||
if (key in secrets) {
|
await request.patch(
|
||||||
if (res[key].value !== secrets[key].value) {
|
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`,
|
||||||
// case: secret value has changed
|
{
|
||||||
updateSecrets.push({
|
|
||||||
id: res[key].id,
|
|
||||||
key,
|
key,
|
||||||
value: secrets[key].value,
|
value: infisicalSecrets[key]?.value,
|
||||||
type: res[key].type,
|
type: res[key].type,
|
||||||
target: res[key].target.includes(integration.targetEnvironment as string)
|
target: res[key].target.includes(integration.targetEnvironment as string)
|
||||||
? [...res[key].target]
|
? [...res[key].target]
|
||||||
@ -1517,28 +1591,24 @@ const syncSecretsVercel = async ({
|
|||||||
gitBranch: integration.path
|
gitBranch: integration.path
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} 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
|
// delete logic
|
||||||
if (newSecrets.length > 0) {
|
for await (const key of Object.keys(res)) {
|
||||||
await request.post(`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`, newSecrets, {
|
if (infisicalSecrets[key] === null) {
|
||||||
|
// case: delete secret
|
||||||
|
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`, {
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
@ -1546,28 +1616,6 @@ const syncSecretsVercel = async ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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, {
|
|
||||||
params,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
integration,
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
secrets,
|
secrets,
|
||||||
accessToken
|
accessToken,
|
||||||
|
createManySecretsRawFn
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case Integrations.NETLIFY:
|
case Integrations.NETLIFY:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import opentelemetry from "@opentelemetry/api";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -158,6 +159,12 @@ export const secretQueueFactory = ({
|
|||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
projectKeyDAL
|
projectKeyDAL
|
||||||
}: TSecretQueueFactoryDep) => {
|
}: 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 removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
await queueService.stopRepeatableJob(
|
await queueService.stopRepeatableJob(
|
||||||
@ -933,6 +940,19 @@ export const secretQueueFactory = ({
|
|||||||
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]`
|
`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 =
|
const message =
|
||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
(err instanceof AxiosError
|
(err instanceof AxiosError
|
||||||
|
@ -51,7 +51,7 @@ const buildSlackPayload = (notification: TSlackNotification) => {
|
|||||||
*Environment*: ${payload.environment}
|
*Environment*: ${payload.environment}
|
||||||
*Secret path*: ${payload.secretPath || "/"}
|
*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
|
payload.requestId
|
||||||
}|here>.`;
|
}|here>.`;
|
||||||
|
|
||||||
|
@ -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.
|
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.
|
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.
|
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.
|
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.
|
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)`.
|
7. Change your Slack username in the users channel to `[NAME] (Infisical)`.
|
||||||
|
@ -144,9 +144,6 @@ services:
|
|||||||
- ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files
|
- ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files
|
||||||
- ./frontend/public:/app/public
|
- ./frontend/public:/app/public
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
|
||||||
- NEXT_PUBLIC_ENV=development
|
|
||||||
- INFISICAL_TELEMETRY_ENABLED=false
|
|
||||||
|
|
||||||
pgadmin:
|
pgadmin:
|
||||||
image: dpage/pgadmin4
|
image: dpage/pgadmin4
|
||||||
|
@ -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>
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/app-connections/aws/{connectionId}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/app-connections/aws/{connectionId}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/app-connections/aws/name/{connectionName}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/app-connections/aws"
|
||||||
|
---
|
@ -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>
|
@ -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>
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/app-connections/github/{connectionId}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/app-connections/github/{connectionId}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/app-connections/github/name/{connectionName}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/app-connections/github"
|
||||||
|
---
|
@ -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>
|
4
docs/api-reference/endpoints/app-connections/list.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/app-connections"
|
||||||
|
---
|
4
docs/api-reference/endpoints/app-connections/options.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Options"
|
||||||
|
openapi: "GET /api/v1/app-connections/options"
|
||||||
|
---
|
116
docs/cli/commands/ssh.mdx
Normal 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>
|
@ -4,6 +4,13 @@ sidebarTitle: "Overview"
|
|||||||
description: "Learn how to generate secrets dynamically on-demand."
|
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
|
## 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**.
|
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**.
|
||||||
|
@ -3,6 +3,13 @@ title: "Approval Workflows"
|
|||||||
description: "Learn how to enable a set of policies to manage changes to sensitive secrets and environments."
|
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
|
## Problem at hand
|
||||||
|
|
||||||
Updating secrets in high-stakes environments (e.g., production) can have a number of problematic issues:
|
Updating secrets in high-stakes environments (e.g., production) can have a number of problematic issues:
|
||||||
|
@ -6,7 +6,7 @@ description: "Learn how to generate SSH credentials to provide secure and centra
|
|||||||
|
|
||||||
## Concept
|
## 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,
|
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.
|
unauthorized access, and SSH key sprawl.
|
||||||
|
|
||||||
@ -192,6 +192,8 @@ infisical login
|
|||||||
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
|
- `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.
|
- `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>
|
||||||
<Step title="SSH into the host">
|
<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.
|
Finally, SSH into the desired host; the SSH operation will be performed using the SSH certificate loaded into the SSH agent.
|
||||||
@ -199,7 +201,6 @@ infisical login
|
|||||||
```bash
|
```bash
|
||||||
ssh username@hostname
|
ssh username@hostname
|
||||||
```
|
```
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|
BIN
docs/images/app-connections/aws/access-key-connection.png
Normal file
After Width: | Height: | Size: 957 KiB |
BIN
docs/images/app-connections/aws/assume-role-connection.png
Normal file
After Width: | Height: | Size: 957 KiB |
BIN
docs/images/app-connections/aws/create-access-key-method.png
Normal file
After Width: | Height: | Size: 598 KiB |
BIN
docs/images/app-connections/aws/create-assume-role-method.png
Normal file
After Width: | Height: | Size: 584 KiB |
BIN
docs/images/app-connections/aws/parameter-store-permissions.png
Normal file
After Width: | Height: | Size: 306 KiB |
BIN
docs/images/app-connections/aws/secrets-manager-permissions.png
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
docs/images/app-connections/aws/select-aws-connection.png
Normal file
After Width: | Height: | Size: 584 KiB |
BIN
docs/images/app-connections/general/add-connection.png
Normal file
After Width: | Height: | Size: 974 KiB |
BIN
docs/images/app-connections/github/create-github-app-method.png
Normal file
After Width: | Height: | Size: 586 KiB |
BIN
docs/images/app-connections/github/create-oauth-method.png
Normal file
After Width: | Height: | Size: 580 KiB |
BIN
docs/images/app-connections/github/github-app-connection.png
Normal file
After Width: | Height: | Size: 974 KiB |
BIN
docs/images/app-connections/github/install-github-app.png
Normal file
After Width: | Height: | Size: 352 KiB |
BIN
docs/images/app-connections/github/oauth-connection.png
Normal file
After Width: | Height: | Size: 974 KiB |
BIN
docs/images/app-connections/github/select-github-connection.png
Normal file
After Width: | Height: | Size: 562 KiB |
Before Width: | Height: | Size: 554 KiB After Width: | Height: | Size: 795 KiB |
After Width: | Height: | Size: 493 KiB |
354
docs/integrations/app-connections/aws.mdx
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
---
|
||||||
|
title: "AWS Connection"
|
||||||
|
description: "Learn how to configure an AWS Connection for Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
Infisical supports two methods for connecting to AWS.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Assume Role (Recommended)">
|
||||||
|
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
<Accordion title="Self-Hosted Instance">
|
||||||
|
To connect your self-hosted Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the configured AWS IAM Role.
|
||||||
|
|
||||||
|
If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2.
|
||||||
|
|
||||||
|
The following steps are for instances not deployed on AWS:
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create an IAM User">
|
||||||
|
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
|
||||||
|
</Step>
|
||||||
|
<Step title="Create an Inline Policy">
|
||||||
|
Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowAssumeAnyRole",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "sts:AssumeRole",
|
||||||
|
"Resource": "arn:aws:iam::*:role/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step title="Obtain the IAM User Credentials">
|
||||||
|
Obtain the AWS access key ID and secret access key for your IAM User by navigating to **IAM > Users > [Your User] > Security credentials > Access keys**.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Set Up Connection Keys">
|
||||||
|
1. Set the access key as **INF_APP_CONNECTION_AWS_CLIENT_ID**.
|
||||||
|
2. Set the secret key as **INF_APP_CONNECTION_AWS_CLIENT_SECRET**.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create the Managing User IAM Role for Infisical">
|
||||||
|
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||||
|
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||||
|
4. Optionally, enable **Require external ID** and enter your **Organization ID** to further enhance security.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add Required Permissions for the IAM Role">
|
||||||
|
Depending on your use case, add one or more of the following policies to your IAM Role:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Secrets Sync">
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="AWS Secrets Manager">
|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowSecretsManagerAccess",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"secretsmanager:GetSecretValue",
|
||||||
|
"secretsmanager:CreateSecret",
|
||||||
|
"secretsmanager:UpdateSecret",
|
||||||
|
"secretsmanager:DescribeSecret",
|
||||||
|
"secretsmanager:TagResource",
|
||||||
|
"secretsmanager:UntagResource",
|
||||||
|
"kms:ListKeys",
|
||||||
|
"kms:ListAliases",
|
||||||
|
"kms:Encrypt",
|
||||||
|
"kms:Decrypt"
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="AWS Parameter Store">
|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowSSMAccess",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"ssm:PutParameter",
|
||||||
|
"ssm:DeleteParameter",
|
||||||
|
"ssm:GetParameters",
|
||||||
|
"ssm:GetParametersByPath",
|
||||||
|
"ssm:DescribeParameters",
|
||||||
|
"ssm:DeleteParameters",
|
||||||
|
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||||
|
"kms:ListKeys", // if you need to specify the KMS key
|
||||||
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
|
"kms:Decrypt" // if you need to specify the KMS key
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Copy the AWS IAM Role ARN">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Setup AWS Connection in Infisical">
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
1. Navigate to the App Connections tab on the Organization Settings page.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select the **AWS Connection** option.
|
||||||
|

|
||||||
|
|
||||||
|
3. Select the **Assume Role** method option and provide the **AWS IAM Role ARN** obtained from the previous step and press **Connect to AWS**.
|
||||||
|

|
||||||
|
|
||||||
|
4. Your **AWS Connection** is now available for use.
|
||||||
|

|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
To create an AWS Connection, make an API request to the [Create AWS
|
||||||
|
Connection](/api-reference/endpoints/app-connections/aws/create) API endpoint.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/app-connections/aws \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"name": "my-aws-connection",
|
||||||
|
"method": "assume-role",
|
||||||
|
"credentials": {
|
||||||
|
"roleArn": "...",
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"appConnection": {
|
||||||
|
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"name": "my-aws-connection",
|
||||||
|
"version": 123,
|
||||||
|
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"createdAt": "2023-11-07T05:31:56Z",
|
||||||
|
"updatedAt": "2023-11-07T05:31:56Z",
|
||||||
|
"app": "aws",
|
||||||
|
"method": "assume-role",
|
||||||
|
"credentials": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Access Key">
|
||||||
|
Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance.
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create the Managing User IAM Role for Infisical">
|
||||||
|
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||||
|
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||||
|
4. Optionally, enable **Require external ID** and enter your **Organization ID** to further enhance security.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add Required Permissions for the IAM Role">
|
||||||
|
Depending on your use case, add one or more of the following policies to your IAM Role:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Secrets Sync">
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="AWS Secrets Manager">
|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowSecretsManagerAccess",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"secretsmanager:GetSecretValue",
|
||||||
|
"secretsmanager:CreateSecret",
|
||||||
|
"secretsmanager:UpdateSecret",
|
||||||
|
"secretsmanager:DescribeSecret",
|
||||||
|
"secretsmanager:TagResource",
|
||||||
|
"secretsmanager:UntagResource",
|
||||||
|
"kms:ListKeys",
|
||||||
|
"kms:ListAliases",
|
||||||
|
"kms:Encrypt",
|
||||||
|
"kms:Decrypt"
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="AWS Parameter Store">
|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowSSMAccess",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"ssm:PutParameter",
|
||||||
|
"ssm:DeleteParameter",
|
||||||
|
"ssm:GetParameters",
|
||||||
|
"ssm:GetParametersByPath",
|
||||||
|
"ssm:DescribeParameters",
|
||||||
|
"ssm:DeleteParameters",
|
||||||
|
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||||
|
"kms:ListKeys", // if you need to specify the KMS key
|
||||||
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
|
"kms:Decrypt" // if you need to specify the KMS key
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Step>
|
||||||
|
<Step title="Obtain Access Key ID and Secret Access Key">
|
||||||
|
Retrieve an AWS **Access Key ID** and a **Secret Key** for your IAM user in **IAM > Users > User > Security credentials > Access keys**.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Setup AWS Connection in Infisical">
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
1. Navigate to the App Connections tab on the Organization Settings page.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select the **AWS Connection** option.
|
||||||
|

|
||||||
|
|
||||||
|
3. Select the **Access Key** method option and provide the **Access Key ID** and **Secret Key** obtained from the previous step and press **Connect to AWS**.
|
||||||
|

|
||||||
|
|
||||||
|
4. Your **AWS Connection** is now available for use.
|
||||||
|

|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
To create an AWS Connection, make an API request to the [Create AWS
|
||||||
|
Connection](/api-reference/endpoints/app-connections/aws/create) API endpoint.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/app-connections/aws \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"name": "my-aws-connection",
|
||||||
|
"method": "access-key",
|
||||||
|
"credentials": {
|
||||||
|
"accessKeyId": "...",
|
||||||
|
"secretKey": "..."
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"appConnection": {
|
||||||
|
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"name": "my-aws-connection",
|
||||||
|
"version": 123,
|
||||||
|
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"createdAt": "2023-11-07T05:31:56Z",
|
||||||
|
"updatedAt": "2023-11-07T05:31:56Z",
|
||||||
|
"app": "aws",
|
||||||
|
"method": "access-key",
|
||||||
|
"credentials": {
|
||||||
|
"accessKeyId": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
169
docs/integrations/app-connections/github.mdx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
title: "GitHub Connection"
|
||||||
|
description: "Learn how to configure a GitHub Connection for Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
Infisical supports two methods for connecting to GitHub.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="GitHub App (Recommended)">
|
||||||
|
Infisical will use a GitHub App with finely grained permissions to connect to GitHub.
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
<Accordion title="Self-Hosted Instance">
|
||||||
|
Using the GitHub integration with app authentication on a self-hosted instance of Infisical requires configuring an application on GitHub
|
||||||
|
and registering your instance with it.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create an application on GitHub">
|
||||||
|
Navigate to the GitHub app settings [here](https://github.com/settings/apps). Click **New GitHub App**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Give the application a name, a homepage URL (your self-hosted domain i.e. `https://your-domain.com`), and a callback URL (i.e. `https://your-domain.com/app-connections/github/oauth/callback`).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Enable request user authorization during app installation.
|
||||||
|

|
||||||
|
|
||||||
|
Disable webhook by unchecking the Active checkbox.
|
||||||
|

|
||||||
|
|
||||||
|
Set the repository permissions as follows: Metadata: Read-only, Secrets: Read and write, Environments: Read and write, Actions: Read.
|
||||||
|

|
||||||
|
|
||||||
|
Similarly, set the organization permissions as follows: Secrets: Read and write.
|
||||||
|

|
||||||
|
|
||||||
|
Create the Github application.
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you have a GitHub organization, you can create an application under it
|
||||||
|
in your organization Settings > Developer settings > GitHub Apps > New GitHub App.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
<Step title="Add your application credentials to Infisical">
|
||||||
|
Generate a new **Client Secret** for your GitHub application.
|
||||||
|

|
||||||
|
|
||||||
|
Generate a new **Private Key** for your Github application.
|
||||||
|

|
||||||
|
|
||||||
|
Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key.
|
||||||
|

|
||||||
|
|
||||||
|
Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application:
|
||||||
|
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID`: The **Client ID** of your GitHub application.
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET`: The **Client Secret** of your GitHub application.
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SLUG`: The **Slug** of your GitHub application. This is the one found in the URL.
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_APP_ID`: The **App ID** of your GitHub application.
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_PRIVATE_KEY`: The **Private Key** of your GitHub application.
|
||||||
|
|
||||||
|
Once added, restart your Infisical instance and use the GitHub integration via app authentication.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
## Setup GitHub Connection in Infisical
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to the App Connections">
|
||||||
|
Navigate to the **App Connections** tab on the **Organization Settings** page.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Add Connection">
|
||||||
|
Select the **GitHub Connection** option from the connection options modal.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Authorize Connection">
|
||||||
|
Select the **GitHub App** method and click **Connect to GitHub**.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Install GitHub App">
|
||||||
|
You will then be redirected to the GitHub app installation page.
|
||||||
|
|
||||||
|
Install and authorize the GitHub application. This will redirect you back to Infisical's App Connections page.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Connection Created">
|
||||||
|
Your **GitHub Connection** is now available for use.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="OAuth">
|
||||||
|
Infisical will use an OAuth App to connect to GitHub.
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
<Accordion title="Self-Hosted Instance">
|
||||||
|
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
|
||||||
|
and registering your instance with it.
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create an OAuth application in GitHub">
|
||||||
|
Navigate to your user Settings > Developer settings > OAuth Apps to create a new GitHub OAuth application.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com`
|
||||||
|
and the **Authorization callback URL** to `https://your-domain.com/app-connections/github/oauth/callback`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you have a GitHub organization, you can create an OAuth application under it
|
||||||
|
in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
<Step title="Add your OAuth application credentials to Infisical">
|
||||||
|
Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application:
|
||||||
|
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID`: The **Client ID** of your GitHub OAuth application.
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET`: The **Client Secret** of your GitHub OAuth application.
|
||||||
|
|
||||||
|
Once added, restart your Infisical instance and use the GitHub integration.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
## Setup GitHub Connection in Infisical
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to the App Connections">
|
||||||
|
Navigate to the **App Connections** tab on the **Organization Settings** page.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Add Connection">
|
||||||
|
Select the **GitHub Connection** option from the connection options modal.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Authorize Connection">
|
||||||
|
Select the **OAuth** method and click **Connect to GitHub**.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Grant Access">
|
||||||
|
You will then be redirected to the GitHub to grant Infisical access to your GitHub account (organization and repo privileges).
|
||||||
|
Once granted, you will redirect you back to Infisical's App Connections page.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Connection Created">
|
||||||
|
Your **GitHub Connection** is now available for use.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
77
docs/integrations/app-connections/overview.mdx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
sidebarTitle: "Overview"
|
||||||
|
description: "Learn how to manage and configure third-party app connections with Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
App Connections enable your organization to integrate Infisical with third-party services in a secure and versatile way.
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
App Connections are an organization-level resource used to establish connections with third-party applications
|
||||||
|
that can be used across Infisical projects. Example use cases include syncing secrets, generating dynamic secrets, and more.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {'flowchart': {'curve': 'linear'} } }%%
|
||||||
|
graph TD
|
||||||
|
A[AWS]
|
||||||
|
B[AWS Connection]
|
||||||
|
C[Project 1 Secret Sync]
|
||||||
|
D[Project 2 Secret Sync]
|
||||||
|
E[Project 3 Generate Dynamic Secret]
|
||||||
|
|
||||||
|
B --> A
|
||||||
|
C --> B
|
||||||
|
D --> B
|
||||||
|
E --> B
|
||||||
|
|
||||||
|
classDef default fill:#ffffff,stroke:#666,stroke-width:2px,rx:10px,color:black
|
||||||
|
classDef aws fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:15px
|
||||||
|
classDef project fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px
|
||||||
|
classDef connection fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px
|
||||||
|
|
||||||
|
class A aws
|
||||||
|
class B connection
|
||||||
|
class C,D,E project
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
App Connections require initial setup in both your third-party application and Infisical. Follow these steps to establish a secure connection:
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
For step-by-step guides specific to each application, refer to the App Connections section in the Navigation Bar.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
1. <strong>Create Access Entity:</strong> If necessary, create an entity such as a service account or role within the third-party application you want to connect to. Be sure
|
||||||
|
to limit the access of this entity to the minimal permission set required to perform the operations you need. For example:
|
||||||
|
- For secret syncing: Read/write permissions to specific secret stores
|
||||||
|
- For dynamic secrets: Permissions to create temporary credentials
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Whenever possible, Infisical encourages creating a designated service account for your App Connection to limit the scope of permissions based on your use-case.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
2. <strong>Generate Authentication Credentials:</strong> Obtain the required credentials from your third-party application. These can vary between applications and might be:
|
||||||
|
- an API key or access token
|
||||||
|
- A client ID and secret pair
|
||||||
|
- other credentials, etc.
|
||||||
|
|
||||||
|
3. <strong>Create App Connection:</strong> Configure the connection in Infisical using your generated credentials through either the UI or API.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Some App Connections can only be created via the UI such as connections using OAuth.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
4. <strong>Utilize the Connection:</strong> Use your App Connection for various features across Infisical such as our Secrets Sync by selecting it via the dropdown menu
|
||||||
|
in the UI or by passing the associated `connectionId` when generating resources via the API.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Infisical is continuously expanding its third-party application support. If your desired application isn't listed,
|
||||||
|
you can still use previous methods of connecting to it such as our Native Integrations.
|
||||||
|
</Note>
|
@ -29,6 +29,36 @@ description: "How to sync secrets from Infisical to Azure App Configuration"
|
|||||||

|

|
||||||
|
|
||||||
Press create integration to start syncing secrets to Azure App Configuration.
|
Press create integration to start syncing secrets to Azure App Configuration.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The Azure App Configuration integration requires the following permissions to be set on the user / service principal
|
||||||
|
for Infisical to sync secrets to Azure App Configuration: `Read Key-Value`, `Write Key-Value`, `Delete Key-Value`.
|
||||||
|
|
||||||
|
Any role with these permissions would work such as the **App Configuration Data Owner** role. Alternatively, you can use the
|
||||||
|
**App Configuration Data Reader** role for read-only access or **App Configuration Data Contributor** role for read/write access.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Additional Configuration">
|
||||||
|
|
||||||
|
#### Azure references
|
||||||
|
When adding secrets in Infisical that reference Azure Key Vault secrets, Infisical will automatically sets the content type to `application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8` in Azure App Configuration.
|
||||||
|
The following reference formats are automatically detected when added on Infisical's side:
|
||||||
|
- `{ "uri": "https://my-key-vault.vault.azure.net/secrets/my-secret" }`
|
||||||
|
- `https://my-key-vault.vault.azure.net/secrets/my-secret`
|
||||||
|
|
||||||
|
#### Azure Labels
|
||||||
|
You can sync secrets from Infisical to Azure with custom labels by enabling the `Use Labels` option during setup:
|
||||||
|
|
||||||
|
**When enabled**: Secrets will be pushed to Azure with your specified label
|
||||||
|
|
||||||
|
**When disabled**: Secrets will be pushed with an empty (null) label
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If you have set the initial sync to `import` have behavior, the label selection affects which secrets are imported from Azure:
|
||||||
|
- With `Use Labels` disabled: Only secrets with empty labels are imported on initial sync
|
||||||
|
- With `Use Labels` enabled: Only secrets matching your specified label are imported on initial sync
|
||||||
|
</Info>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|