Compare commits

..

146 Commits

Author SHA1 Message Date
2e02f8bea8 Merge pull request #3199 from akhilmhdh/feat/webhook-reminder
Added webhook trigger for secret reminder
2025-03-07 14:17:11 -05:00
8203158c63 Merge pull request #3195 from Infisical/feat/addSecretNameToSlackNotification
Feat/add secret name to slack notification
2025-03-07 15:39:06 -03:00
cc9cc70125 Merge pull request #3203 from Infisical/misc/add-uncaught-exception-handler
misc: add uncaught exception handler
2025-03-08 00:36:08 +08:00
045debeaf3 misc: added unhandled rejection handler 2025-03-08 00:29:23 +08:00
3fb8ad2fac misc: add uncaught exception handler 2025-03-08 00:22:27 +08:00
cbe3acde74 Merge pull request #3202 from Infisical/fix/address-unhandled-promise-rejects-causing-502
fix: address unhandled promise rejects causing 502s
2025-03-07 23:48:43 +08:00
de480b5771 Merge pull request #3181 from Infisical/daniel/id-get-secret
feat: get secret by ID
2025-03-07 19:35:52 +04:00
07b93c5cec Update secret-v2-bridge-service.ts 2025-03-07 19:26:18 +04:00
77431b4719 requested changes 2025-03-07 19:26:18 +04:00
50610945be feat: get secret by ID 2025-03-07 19:25:53 +04:00
57f54440d6 misc: added support for type 2025-03-07 23:15:05 +08:00
9711e73a06 fix: address unhandled promise rejects causing 502s 2025-03-07 23:05:47 +08:00
58ebebb162 Merge pull request #3191 from Infisical/feat/addActorToVersionHistory
Add actor to secret version history
2025-03-07 08:06:24 -03:00
65ddddb6de Change slack notification label from key to secret key 2025-03-07 08:03:02 -03:00
=
a55b26164a feat: updated doc 2025-03-07 15:14:09 +05:30
=
6cd448b8a5 feat: webhook on secret reminder trigger 2025-03-07 15:01:14 +05:30
b7640f2d03 Lint fixes 2025-03-06 17:36:09 -03:00
2ee4d68fd0 Fix case for multiple projects messing with the joins 2025-03-06 17:04:01 -03:00
3ca931acf1 Add condition to query to only retrieve the actual project id 2025-03-06 16:38:49 -03:00
7f6715643d Change label from Secret to Key for consistency with the UI 2025-03-06 15:31:37 -03:00
8e311658d4 Improve query to only use one to retrieve all information 2025-03-06 15:15:52 -03:00
9116acd37b Fix linter issues 2025-03-06 13:07:03 -03:00
0513307d98 Improve code quality 2025-03-06 12:55:10 -03:00
28c2f1874e Add secret name to slack notification 2025-03-06 12:46:43 -03:00
efc3b6d474 Remove secret_version_v1 changes 2025-03-06 11:31:26 -03:00
07e1d1b130 Merge branch 'main' into feat/addActorToVersionHistory 2025-03-06 10:56:54 -03:00
7f76779124 Fix frontend type errors 2025-03-06 09:17:55 -03:00
30bcf1f204 Fix linter and type issues, made a small fix for secret rotation platform events 2025-03-06 09:10:13 -03:00
706feafbf2 revert featureset changes 2025-03-06 00:20:08 -05:00
fc4e3f1f72 update relay health check 2025-03-05 23:50:11 -05:00
dcd5f20325 add example 2025-03-05 22:20:13 -05:00
58f3e116a3 add example 2025-03-05 22:19:56 -05:00
7bc5aad8ec fix infinite loop 2025-03-05 22:14:09 -05:00
a16dc3aef6 add windows stub to fix build issue 2025-03-05 18:29:29 -05:00
da7746c639 use forked pion 2025-03-05 17:54:23 -05:00
cd5b6da541 Merge branch 'main' into feat/addActorToVersionHistory 2025-03-05 17:53:57 -03:00
2dda7180a9 Fix linter issue 2025-03-05 17:36:00 -03:00
30ccfbfc8e Add actor to secret version history 2025-03-05 17:20:57 -03:00
aa76924ee6 fix import 2025-03-05 14:48:36 -05:00
d8f679e72d Merge pull request #3189 from Infisical/revert-3128-daniel/view-secret-value-permission
Revert "feat(api/secrets): view secret value permission"
2025-03-05 14:15:16 -05:00
bf6cfbac7a Revert "feat(api/secrets): view secret value permission" 2025-03-05 14:15:02 -05:00
8e82813894 Merge pull request #3128 from Infisical/daniel/view-secret-value-permission
feat(api/secrets): view secret value permission
2025-03-05 22:57:25 +04:00
df21a1fb81 fix: types 2025-03-05 22:47:40 +04:00
bdbb6346cb fix: permission error instead of not found error on single secret import 2025-03-05 22:47:40 +04:00
ea9da6d2a8 fix: view secret value (requested changes) 2025-03-05 22:47:40 +04:00
3c2c70912f Update secret-service.ts 2025-03-05 22:47:40 +04:00
b607429b99 chore: minor ui improvements 2025-03-05 22:47:40 +04:00
16c1516979 fix: move permissions 2025-03-05 22:47:40 +04:00
f5dbbaf1fd Update SecretEditRow.tsx 2025-03-05 22:47:40 +04:00
2a292455ef chore: minor ui improvements 2025-03-05 22:47:40 +04:00
4d040706a9 Update SecretDetailSidebar.tsx 2025-03-05 22:47:40 +04:00
5183f76397 fix: pathing 2025-03-05 22:47:40 +04:00
4b3efb43b0 fix: view secret value permission (requested changes) 2025-03-05 22:47:40 +04:00
96046726b2 Update 20250218020306_backfill-secret-permissions-with-readvalue.ts 2025-03-05 22:47:40 +04:00
a86a951acc Update secret-snapshot-service.ts 2025-03-05 22:47:40 +04:00
5e70860160 fix: ui bug 2025-03-05 22:47:40 +04:00
abbd427ee2 minor lint fixes 2025-03-05 22:47:40 +04:00
8fd5fdbc6a chore: minor changes 2025-03-05 22:47:40 +04:00
77e1ccc8d7 fix: view secret value permission (requested changes) 2025-03-05 22:47:40 +04:00
711cc438f6 chore: better error 2025-03-05 22:47:40 +04:00
8447190bf8 fix: coderabbit requested changes 2025-03-05 22:47:40 +04:00
12b447425b chore: further cleanup 2025-03-05 22:47:40 +04:00
9cb1a31287 fix: allow Viewer role to read value 2025-03-05 22:47:40 +04:00
b00413817d fix: add service token read value permissions 2025-03-05 22:47:40 +04:00
2a8bd74e88 Update 20250218020306_backfill-secret-permissions-with-readvalue.ts 2025-03-05 22:47:40 +04:00
f28f4f7561 fix: requested changes 2025-03-05 22:47:40 +04:00
f0b05c683b fix: service token creation 2025-03-05 22:47:40 +04:00
3e8f02a4f9 Update service-token.spec.ts 2025-03-05 22:47:40 +04:00
50ee60a3ea Update service-token.spec.ts 2025-03-05 22:47:40 +04:00
21bdecdf2a Update secret-v2-bridge-service.ts 2025-03-05 22:47:40 +04:00
bf09461416 Update secret-v2-bridge-service.ts 2025-03-05 22:47:40 +04:00
1ff615913c fix: bulk secret create 2025-03-05 22:47:40 +04:00
281cedf1a2 fix: updated migration to support additional privileges 2025-03-05 22:47:39 +04:00
a8d847f139 chore: remove logs 2025-03-05 22:47:39 +04:00
2a0c0590f1 fix: cleanup and bug fixes 2025-03-05 22:47:39 +04:00
2e6d525d27 chore: cleanup 2025-03-05 22:47:39 +04:00
7fd4249d00 fix: frontend requested changes 2025-03-05 22:47:39 +04:00
90cfc44592 fix: personal secret support without read value permission 2025-03-05 22:47:39 +04:00
8c403780c2 chore: lint & ts 2025-03-05 22:47:39 +04:00
b69c091f2f Update 20250218020306_backfill-secret-permissions-with-readvalue.ts 2025-03-05 22:47:39 +04:00
4a66395ce6 feat(api): view secret value, WIP 2025-03-05 22:47:39 +04:00
8c18753e3f Merge pull request #3188 from Infisical/daniel/fix-breaking-check
fix: breaking changes check
2025-03-05 22:45:56 +04:00
85c5d69c36 chore: remove breaking change test 2025-03-05 22:42:29 +04:00
94fe577046 chore: test breaking change 2025-03-05 22:38:35 +04:00
a0a579834c fix: check docs endpoint instead of status 2025-03-05 22:36:43 +04:00
b5575f4c20 fix api endpoint 2025-03-05 22:31:01 +04:00
f98f212ecf Update check-api-for-breaking-changes.yml 2025-03-05 22:23:49 +04:00
b331a4a708 fix: breaking changes check 2025-03-05 22:17:16 +04:00
e351a16b5a Merge pull request #3184 from Infisical/feat/add-secret-approval-review-comment
feat: add secret approval review comment
2025-03-05 12:24:59 -05:00
2cfca823f2 Merge pull request #3187 from akhilmhdh/feat/connector
feat: added ca to cli
2025-03-05 10:13:27 -05:00
=
a8398a7009 feat: added ca to cli 2025-03-05 20:00:45 +05:30
8c054cedfc misc: added section for approval and rejections 2025-03-05 22:30:26 +08:00
24d4f8100c Merge pull request #3183 from akhilmhdh/feat/connector
feat: fixed cli issues in gateway
2025-03-05 08:26:04 -05:00
08f23e2d3c remove background context 2025-03-05 08:24:56 -05:00
d1ad605ac4 misc: address nit 2025-03-05 21:19:41 +08:00
9dd5857ff5 misc: minor UI 2025-03-05 19:32:26 +08:00
babbacdc96 feat: add secret approval review comment 2025-03-05 19:25:56 +08:00
=
76427f43f7 feat: fixed cli issues in gateway 2025-03-05 16:16:07 +05:30
3badcea95b added permission refresh and main context 2025-03-05 01:07:36 -05:00
1a4c0fe8d9 make heartbeat method simple + fix import 2025-03-04 23:21:26 -05:00
04f6864abc Merge pull request #3177 from Infisical/improve-secret-scanning-setup
Improvement: Clear Secret Scanning Query Params after Setup
2025-03-05 04:05:38 +04:00
fcbe0f59d2 Merge pull request #3180 from Infisical/daniel/fix-vercel-custom-envs
fix: vercel integration custom envs
2025-03-04 13:45:48 -08:00
e95b6fdeaa cleanup 2025-03-05 01:36:06 +04:00
5391bcd3b2 fix: vercel integration custom envs 2025-03-05 01:33:58 +04:00
48fd9e2a56 Merge pull request #3179 from akhilmhdh/feat/connector
feat: quick fix for quic
2025-03-04 15:52:48 -05:00
=
7b5926d865 feat: quick fix for quic 2025-03-05 02:14:00 +05:30
034123bcdf Merge pull request #3175 from Infisical/feat/grantServerAdminAccessToUsers
Allow server admins to grant server admin access to other users
2025-03-04 15:25:09 -05:00
f3786788fd Improve UserPanelTable, moved from useState to handlePopUpOpen 2025-03-04 16:54:28 -03:00
c406f6d78d Update release_build_infisical_cli.yml 2025-03-04 14:52:01 -05:00
eb66295dd4 Update release_build_infisical_cli.yml 2025-03-04 14:41:44 -05:00
798215e84c Update release_build_infisical_cli.yml 2025-03-04 14:36:39 -05:00
53f7491441 Update UpgradePlanModal message to show relevant message on user actions 2025-03-04 16:30:22 -03:00
53f6ab118b Merge pull request #3178 from akhilmhdh/feat/connector
Add QUIC to gateway
2025-03-04 14:06:42 -05:00
=
0f5a1b13a6 fix: lint and typecheck 2025-03-05 00:33:28 +05:30
5c606fe45f improvement: replace window reload with query refetch 2025-03-04 10:39:40 -08:00
bbf60169eb Update Server Admin Console documentation and add a fix for endpoint /admin-access 2025-03-04 15:29:34 -03:00
=
e004be22e3 feat: updated docker image and resolved build error 2025-03-04 23:58:31 +05:30
=
016cb4a7ba feat: completed gateway in quic mode 2025-03-04 23:55:40 +05:30
=
9bfc2a5dd2 feat: updated gateway to quic 2025-03-04 23:55:40 +05:30
72dbef97fb improvement: clear query params after setup to avoid false error messages 2025-03-04 10:14:56 -08:00
f376eaae13 Merge pull request #3174 from Infisical/feat/addFolderDescription
Add descriptions to secret folders
2025-03-04 14:56:43 -03:00
026f883d21 Merge pull request #3176 from Infisical/misc/replaced-otel-auto-instrumentation-with-manual
misc: replaced otel auto instrumentation with manual
2025-03-04 12:24:14 -05:00
e42f860261 misc: removed host metrics 2025-03-05 01:20:06 +08:00
08ec8c9b73 Fix linter issue and remove background colors from dropdown list 2025-03-04 13:58:34 -03:00
1512d4f496 Fix folder empty description issue and added icon to display it 2025-03-04 13:44:40 -03:00
9f7b42ad91 misc: replaced otel auto instrumentation with manual 2025-03-05 00:16:15 +08:00
3045477c32 Merge pull request #3169 from Infisical/bitbucket-workspace-select-fix
Fix: Address Bitbucket Configuration UI Bug Preventing Workspace Selection
2025-03-05 01:14:09 +09:00
be4adc2759 Allow server admins to grant server admin access to other users 2025-03-04 12:38:27 -03:00
4eba80905a Lint fixes 2025-03-04 10:44:26 -03:00
b023bc7442 Type fixes 2025-03-04 10:26:23 -03:00
a0029ab469 Add descriptions to secret folders 2025-03-04 10:11:20 -03:00
53605c3880 improvement: address feedback 2025-03-03 15:11:48 -08:00
e5bca5b5df Merge pull request #3171 from Infisical/remove-mention-of-affixes-for-secret-syncs
Documentation: Remove Secret Sync Affix Options Reference
2025-03-03 14:51:56 -08:00
4091bc19e9 Merge pull request #3172 from Infisical/fix/secretReminderSubmitOnModalClose
Save Secret Reminder from Modal
2025-03-03 15:25:42 -05:00
23bd048bb9 Fix delete secret reminder notification 2025-03-03 17:20:44 -03:00
17a4674821 Fix success notification message on reminder updates 2025-03-03 17:04:02 -03:00
ec9631107d Type fixes 2025-03-03 16:36:14 -03:00
3fa450b9a7 Fix for secrets reminder modal, now saving the reminder on modal close 2025-03-03 16:13:03 -03:00
3b9c62c366 Merge pull request #3153 from Infisical/daniel/secret-requests
feat(secret-sharing): secret requests
2025-03-04 04:04:39 +09:00
cb3d171d48 documentation: remove reference to secret affixes in secret syncs overview (temp) 2025-03-03 10:59:31 -08:00
4382825162 fix: address ui preventing from selecting non-default workspace 2025-03-03 10:16:15 -08:00
787c091948 requested changes 2025-03-03 21:44:40 +04:00
ff269b1063 Update RequestedSecretsRow.tsx 2025-03-03 21:14:40 +04:00
ca0636cb25 minor fixes 2025-03-03 21:14:40 +04:00
b995358b7e fix: type fixes 2025-03-03 21:14:40 +04:00
7aaf0f4ed3 feat(secret-sharing): secret requests 2025-03-03 21:14:40 +04:00
129 changed files with 4677 additions and 1849 deletions

View File

@ -32,10 +32,23 @@ jobs:
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start the server - name: Start the server
run: | run: |
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET -e ENCRYPTION_KEY=$ENCRYPTION_KEY --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
echo "Examining built image:"
docker image inspect infisical-api | grep -A 5 "Entrypoint"
docker run --name infisical-api -d -p 4000:4000 \
-e DB_CONNECTION_URI=$DB_CONNECTION_URI \
-e REDIS_URL=$REDIS_URL \
-e JWT_AUTH_SECRET=$JWT_AUTH_SECRET \
-e ENCRYPTION_KEY=$ENCRYPTION_KEY \
--env-file .env \
infisical-api
echo "Container status right after creation:"
docker ps -a | grep infisical-api
env: env:
REDIS_URL: redis://172.17.0.1:6379 REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
@ -43,27 +56,39 @@ jobs:
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218 ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: '1.21.5' go-version: "1.21.5"
- name: Wait for container to be stable and check logs - name: Wait for container to be stable and check logs
run: | run: |
SECONDS=0 SECONDS=0
HEALTHY=0 HEALTHY=0
while [ $SECONDS -lt 60 ]; do while [ $SECONDS -lt 60 ]; do
if docker ps | grep infisical-api | grep -q healthy; then # Check if container is running
echo "Container is healthy." if docker ps | grep infisical-api; then
HEALTHY=1 # Try to access the API endpoint
if curl -s -f http://localhost:4000/api/docs/json > /dev/null 2>&1; then
echo "API endpoint is responding. Container seems healthy."
HEALTHY=1
break
fi
else
echo "Container is not running!"
docker ps -a | grep infisical-api
break break
fi fi
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)" echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
sleep 5
docker logs infisical-api SECONDS=$((SECONDS+5))
sleep 2
SECONDS=$((SECONDS+2))
done done
if [ $HEALTHY -ne 1 ]; then if [ $HEALTHY -ne 1 ]; then
echo "Container did not become healthy in time" echo "Container did not become healthy in time"
echo "Container status:"
docker ps -a | grep infisical-api
echo "Container logs (if any):"
docker logs infisical-api || echo "No logs available"
echo "Container inspection:"
docker inspect infisical-api | grep -A 5 "State"
exit 1 exit 1
fi fi
- name: Install openapi-diff - name: Install openapi-diff
@ -71,7 +96,8 @@ jobs:
- name: Running OpenAPI Spec diff action - name: Running OpenAPI Spec diff action
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup - name: cleanup
if: always()
run: | run: |
docker compose -f "docker-compose.dev.yml" down docker compose -f "docker-compose.dev.yml" down
docker stop infisical-api docker stop infisical-api || true
docker remove infisical-api docker rm infisical-api || true

View File

@ -26,7 +26,7 @@ jobs:
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
npm-release: npm-release:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
env: env:
working-directory: ./npm working-directory: ./npm
needs: needs:
@ -83,7 +83,7 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
goreleaser: goreleaser:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
needs: [cli-integration-tests] needs: [cli-integration-tests]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -103,11 +103,12 @@ jobs:
go-version: ">=1.19.3" go-version: ">=1.19.3"
cache: true cache: true
cache-dependency-path: cli/go.sum cache-dependency-path: cli/go.sum
- name: libssl1.1 => libssl1.0-dev for OSXCross - name: Setup for libssl1.0-dev
run: | run: |
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
sudo apt update && apt-cache policy libssl1.0-dev sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
sudo apt-get install libssl1.0-dev sudo apt update
sudo apt-get install -y libssl1.0-dev
- name: OSXCross for CGO Support - name: OSXCross for CGO Support
run: | run: |
mkdir ../../osxcross mkdir ../../osxcross

View File

@ -3,13 +3,10 @@ ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id ARG INTERCOM_ID=intercom-id
ARG CAPTCHA_SITE_KEY=captcha-site-key ARG CAPTCHA_SITE_KEY=captcha-site-key
FROM node:20-alpine AS base FROM node:20-slim AS base
FROM base AS frontend-dependencies FROM base AS frontend-dependencies
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
COPY frontend/package.json frontend/package-lock.json ./ COPY frontend/package.json frontend/package-lock.json ./
@ -45,8 +42,8 @@ RUN npm run build
FROM base AS frontend-runner FROM base AS frontend-runner
WORKDIR /app WORKDIR /app
RUN addgroup --system --gid 1001 nodejs RUN groupadd --system --gid 1001 nodejs
RUN adduser --system --uid 1001 non-root-user RUN useradd --system --uid 1001 --gid nodejs non-root-user
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./ COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
@ -56,21 +53,23 @@ USER non-root-user
## BACKEND ## BACKEND
## ##
FROM base AS backend-build FROM base AS backend-build
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
WORKDIR /app WORKDIR /app
# Install all required dependencies for build # Install all required dependencies for build
RUN apk --update add \ RUN apt-get update && apt-get install -y \
python3 \ python3 \
make \ make \
g++ \ g++ \
unixodbc \ unixodbc \
freetds \ freetds-bin \
unixodbc-dev \ unixodbc-dev \
libc-dev \ libc-dev \
freetds-dev freetds-dev \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --system --gid 1001 nodejs
RUN useradd --system --uid 1001 --gid nodejs non-root-user
COPY backend/package*.json ./ COPY backend/package*.json ./
RUN npm ci --only-production RUN npm ci --only-production
@ -86,18 +85,19 @@ FROM base AS backend-runner
WORKDIR /app WORKDIR /app
# Install all required dependencies for runtime # Install all required dependencies for runtime
RUN apk --update add \ RUN apt-get update && apt-get install -y \
python3 \ python3 \
make \ make \
g++ \ g++ \
unixodbc \ unixodbc \
freetds \ freetds-bin \
unixodbc-dev \ unixodbc-dev \
libc-dev \ libc-dev \
freetds-dev freetds-dev \
&& rm -rf /var/lib/apt/lists/*
# Configure ODBC # Configure ODBC
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
COPY backend/package*.json ./ COPY backend/package*.json ./
RUN npm ci --only-production RUN npm ci --only-production
@ -109,34 +109,36 @@ RUN mkdir frontend-build
# Production stage # Production stage
FROM base AS production FROM base AS production
RUN apk add --upgrade --no-cache ca-certificates RUN apt-get update && apt-get install -y \
RUN apk add --no-cache bash curl && curl -1sLf \ ca-certificates \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ bash \
&& apk add infisical=0.31.1 && apk add --no-cache git curl \
git \
WORKDIR /
# Install all required runtime dependencies
RUN apk --update add \
python3 \ python3 \
make \ make \
g++ \ g++ \
unixodbc \ unixodbc \
freetds \ freetds-bin \
unixodbc-dev \ unixodbc-dev \
libc-dev \ libc-dev \
freetds-dev \ freetds-dev \
bash \ wget \
curl \ openssh-client \
git \ && rm -rf /var/lib/apt/lists/*
openssh
# Install Infisical CLI
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
&& apt-get update && apt-get install -y infisical=0.31.1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /
# Configure ODBC in production # Configure ODBC in production
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
# Setup user permissions # Setup user permissions
RUN addgroup --system --gid 1001 nodejs \ RUN groupadd --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user && useradd --system --uid 1001 --gid nodejs non-root-user
# Give non-root-user permission to update SSL certs # Give non-root-user permission to update SSL certs
RUN chown -R non-root-user /etc/ssl/certs RUN chown -R non-root-user /etc/ssl/certs
@ -154,9 +156,7 @@ ENV INTERCOM_ID=$INTERCOM_ID
ARG CAPTCHA_SITE_KEY ARG CAPTCHA_SITE_KEY
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
COPY --from=backend-runner /app /backend COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build COPY --from=frontend-runner /app ./backend/frontend-build
ARG INFISICAL_PLATFORM_VERSION ARG INFISICAL_PLATFORM_VERSION

View File

@ -1,23 +1,22 @@
# Build stage # Build stage
FROM node:20-alpine AS build FROM node:20-slim AS build
WORKDIR /app WORKDIR /app
# Required for pkcs11js # Required for pkcs11js
RUN apk --update add \ RUN apt-get update && apt-get install -y \
python3 \ python3 \
make \ make \
g++ \ g++ \
openssh openssh-client
# install dependencies for TDS driver (required for SAP ASE dynamic secrets) # Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \ RUN apt-get install -y \
unixodbc \ unixodbc \
freetds \ freetds-bin \
freetds-dev \
unixodbc-dev \ unixodbc-dev \
libc-dev \ libc-dev
freetds-dev
COPY package*.json ./ COPY package*.json ./
RUN npm ci --only-production RUN npm ci --only-production
@ -26,36 +25,36 @@ COPY . .
RUN npm run build RUN npm run build
# Production stage # Production stage
FROM node:20-alpine FROM node:20-slim
WORKDIR /app WORKDIR /app
ENV npm_config_cache /home/node/.npm ENV npm_config_cache /home/node/.npm
COPY package*.json ./ COPY package*.json ./
RUN apk --update add \ RUN apt-get update && apt-get install -y \
python3 \ python3 \
make \ make \
g++ g++
# install dependencies for TDS driver (required for SAP ASE dynamic secrets) # Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \ RUN apt-get install -y \
unixodbc \ unixodbc \
freetds \ freetds-bin \
freetds-dev \
unixodbc-dev \ unixodbc-dev \
libc-dev \ libc-dev
freetds-dev
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
RUN npm ci --only-production && npm cache clean --force RUN npm ci --only-production && npm cache clean --force
COPY --from=build /app . COPY --from=build /app .
RUN apk add --no-cache bash curl && curl -1sLf \ # Install Infisical CLI
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ RUN apt-get install -y curl bash && \
&& apk add infisical=0.8.1 && apk add --no-cache git curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
apt-get update && apt-get install -y infisical=0.8.1 git
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js CMD node healthcheck.js

View File

@ -1,4 +1,4 @@
FROM node:20-alpine FROM node:20-slim
# ? Setup a test SoftHSM module. In production a real HSM is used. # ? Setup a test SoftHSM module. In production a real HSM is used.
@ -7,32 +7,32 @@ ARG SOFTHSM2_VERSION=2.5.0
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
SOFTHSM2_SOURCES=/tmp/softhsm2 SOFTHSM2_SOURCES=/tmp/softhsm2
# install build dependencies including python3 (required for pkcs11js and partially TDS driver) # Install build dependencies including python3 (required for pkcs11js and partially TDS driver)
RUN apk --update add \ RUN apt-get update && apt-get install -y \
alpine-sdk \ build-essential \
autoconf \ autoconf \
automake \ automake \
git \ git \
libtool \ libtool \
openssl-dev \ libssl-dev \
python3 \ python3 \
make \ make \
g++ \ g++ \
openssh openssh-client \
curl \
pkg-config
# install dependencies for TDS driver (required for SAP ASE dynamic secrets) # Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \ RUN apt-get install -y \
unixodbc \ unixodbc \
freetds \
unixodbc-dev \ unixodbc-dev \
libc-dev \ freetds-dev \
freetds-dev freetds-bin \
tdsodbc
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini # Build and install SoftHSM2
# build and install SoftHSM2
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES} RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
WORKDIR ${SOFTHSM2_SOURCES} WORKDIR ${SOFTHSM2_SOURCES}
@ -45,16 +45,18 @@ RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \
WORKDIR /root WORKDIR /root
RUN rm -fr ${SOFTHSM2_SOURCES} RUN rm -fr ${SOFTHSM2_SOURCES}
# install pkcs11-tool # Install pkcs11-tool
RUN apk --update add opensc RUN apt-get install -y opensc
RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 RUN mkdir -p /etc/softhsm2/tokens && \
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
# ? App setup # ? App setup
RUN apk add --no-cache bash curl && curl -1sLf \ # Install Infisical CLI
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
&& apk add infisical=0.8.1 && apk add --no-cache git apt-get update && \
apt-get install -y infisical=0.8.1
WORKDIR /app WORKDIR /app

1325
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -145,6 +145,7 @@
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0", "@google-cloud/kms": "^4.5.0",
"@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/auth-app": "^7.1.1", "@octokit/auth-app": "^7.1.1",
"@octokit/plugin-retry": "^5.0.5", "@octokit/plugin-retry": "^5.0.5",
@ -152,10 +153,10 @@
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@octopusdeploy/api-client": "^3.4.1", "@octopusdeploy/api-client": "^3.4.1",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
"@opentelemetry/exporter-prometheus": "^0.55.0", "@opentelemetry/exporter-prometheus": "^0.55.0",
"@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/instrumentation": "^0.55.0",
"@opentelemetry/instrumentation-http": "^0.57.2",
"@opentelemetry/resources": "^1.28.0", "@opentelemetry/resources": "^1.28.0",
"@opentelemetry/sdk-metrics": "^1.28.0", "@opentelemetry/sdk-metrics": "^1.28.0",
"@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/semantic-conventions": "^1.27.0",

View File

@ -0,0 +1,25 @@
import { Knex } from "knex";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
if (!hasSharingTypeColumn) {
table.string("type", 32).defaultTo(SecretSharingType.Share).notNullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
if (hasSharingTypeColumn) {
table.dropColumn("type");
}
});
}

View File

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
if (!hasProjectDescription) {
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
t.string("description");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
if (hasProjectDescription) {
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
t.dropColumn("description");
});
}
}

View File

@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment"))) {
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
t.string("comment");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment")) {
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
t.dropColumn("comment");
});
}
}

View File

@ -0,0 +1,45 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
if (!hasSecretVersionV2UserActorId) {
t.uuid("userActorId");
t.foreign("userActorId").references("id").inTable(TableName.Users);
}
if (!hasSecretVersionV2IdentityActorId) {
t.uuid("identityActorId");
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
}
if (!hasSecretVersionV2ActorType) {
t.string("actorType");
}
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
if (hasSecretVersionV2UserActorId) {
t.dropColumn("userActorId");
}
if (hasSecretVersionV2IdentityActorId) {
t.dropColumn("identityActorId");
}
if (hasSecretVersionV2ActorType) {
t.dropColumn("actorType");
}
});
}
}

View File

@ -13,7 +13,8 @@ export const SecretApprovalRequestsReviewersSchema = z.object({
requestId: z.string().uuid(), requestId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
reviewerUserId: z.string().uuid() reviewerUserId: z.string().uuid(),
comment: z.string().nullable().optional()
}); });
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>; export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;

View File

@ -15,7 +15,8 @@ export const SecretFoldersSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
envId: z.string().uuid(), envId: z.string().uuid(),
parentId: z.string().uuid().nullable().optional(), parentId: z.string().uuid().nullable().optional(),
isReserved: z.boolean().default(false).nullable().optional() isReserved: z.boolean().default(false).nullable().optional(),
description: z.string().nullable().optional()
}); });
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>; export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;

View File

@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({ export const SecretSharingSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
encryptedValue: z.string().nullable().optional(), encryptedValue: z.string().nullable().optional(),
type: z.string(),
iv: z.string().nullable().optional(), iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(), tag: z.string().nullable().optional(),
hashedHex: z.string().nullable().optional(), hashedHex: z.string().nullable().optional(),

View File

@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
folderId: z.string().uuid(), folderId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(), userId: z.string().uuid().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
userActorId: z.string().uuid().nullable().optional(),
identityActorId: z.string().uuid().nullable().optional(),
actorType: z.string().nullable().optional()
}); });
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>; export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;

View File

@ -159,7 +159,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
id: z.string() id: z.string()
}), }),
body: z.object({ body: z.object({
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]),
comment: z.string().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -175,8 +176,25 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
approvalId: req.params.id, approvalId: req.params.id,
status: req.body.status status: req.body.status,
comment: req.body.comment
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: review.projectId,
event: {
type: EventType.SECRET_APPROVAL_REQUEST_REVIEW,
metadata: {
secretApprovalRequestId: review.requestId,
reviewedBy: review.reviewerUserId,
status: review.status as ApprovalStatus,
comment: review.comment || ""
}
}
});
return { review }; return { review };
} }
}); });
@ -235,7 +253,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const tagSchema = SecretTagsSchema.pick({ const tagSchema = SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}) })
.array() .array()
@ -268,7 +285,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
environment: z.string(), environment: z.string(),
statusChangedByUser: approvalRequestUser.optional(), statusChangedByUser: approvalRequestUser.optional(),
committerUser: approvalRequestUser, committerUser: approvalRequestUser,
reviewers: approvalRequestUser.extend({ status: z.string() }).array(), reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
secretPath: z.string(), secretPath: z.string(),
commits: secretRawSchema commits: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .omit({ _id: true, environment: true, workspace: true, type: true, version: true })

View File

@ -35,7 +35,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}).array() }).array()
}) })

View File

@ -22,6 +22,7 @@ import {
} from "@app/services/secret-sync/secret-sync-types"; } from "@app/services/secret-sync/secret-sync-types";
import { KmipPermission } from "../kmip/kmip-enum"; import { KmipPermission } from "../kmip/kmip-enum";
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
export type TListProjectAuditLogDTO = { export type TListProjectAuditLogDTO = {
filter: { filter: {
@ -165,6 +166,7 @@ export enum EventType {
SECRET_APPROVAL_REQUEST = "secret-approval-request", SECRET_APPROVAL_REQUEST = "secret-approval-request",
SECRET_APPROVAL_CLOSED = "secret-approval-closed", SECRET_APPROVAL_CLOSED = "secret-approval-closed",
SECRET_APPROVAL_REOPENED = "secret-approval-reopened", SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review",
SIGN_SSH_KEY = "sign-ssh-key", SIGN_SSH_KEY = "sign-ssh-key",
ISSUE_SSH_CREDS = "issue-ssh-creds", ISSUE_SSH_CREDS = "issue-ssh-creds",
CREATE_SSH_CA = "create-ssh-certificate-authority", CREATE_SSH_CA = "create-ssh-certificate-authority",
@ -250,6 +252,7 @@ export enum EventType {
UPDATE_APP_CONNECTION = "update-app-connection", UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection", DELETE_APP_CONNECTION = "delete-app-connection",
CREATE_SHARED_SECRET = "create-shared-secret", CREATE_SHARED_SECRET = "create-shared-secret",
CREATE_SECRET_REQUEST = "create-secret-request",
DELETE_SHARED_SECRET = "delete-shared-secret", DELETE_SHARED_SECRET = "delete-shared-secret",
READ_SHARED_SECRET = "read-shared-secret", READ_SHARED_SECRET = "read-shared-secret",
GET_SECRET_SYNCS = "get-secret-syncs", GET_SECRET_SYNCS = "get-secret-syncs",
@ -1141,6 +1144,7 @@ interface CreateFolderEvent {
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
description?: string;
}; };
} }
@ -1312,6 +1316,16 @@ interface SecretApprovalRequest {
}; };
} }
interface SecretApprovalRequestReview {
type: EventType.SECRET_APPROVAL_REQUEST_REVIEW;
metadata: {
secretApprovalRequestId: string;
reviewedBy: string;
status: ApprovalStatus;
comment: string;
};
}
interface SignSshKey { interface SignSshKey {
type: EventType.SIGN_SSH_KEY; type: EventType.SIGN_SSH_KEY;
metadata: { metadata: {
@ -2020,6 +2034,15 @@ interface CreateSharedSecretEvent {
}; };
} }
interface CreateSecretRequestEvent {
type: EventType.CREATE_SECRET_REQUEST;
metadata: {
id: string;
accessType: string;
name?: string;
};
}
interface DeleteSharedSecretEvent { interface DeleteSharedSecretEvent {
type: EventType.DELETE_SHARED_SECRET; type: EventType.DELETE_SHARED_SECRET;
metadata: { metadata: {
@ -2470,4 +2493,6 @@ export type Event =
| KmipOperationActivateEvent | KmipOperationActivateEvent
| KmipOperationRevokeEvent | KmipOperationRevokeEvent
| KmipOperationLocateEvent | KmipOperationLocateEvent
| KmipOperationRegisterEvent; | KmipOperationRegisterEvent
| CreateSecretRequestEvent
| SecretApprovalRequestReview;

View File

@ -86,7 +86,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
tlsOptions: { tlsOptions: {
ca: relayDetails.certChain, ca: relayDetails.certChain,
cert: relayDetails.certificate, cert: relayDetails.certificate,
key: relayDetails.privateKey key: relayDetails.privateKey.toString()
} }
} }
); );

View File

@ -474,7 +474,7 @@ export const gatewayServiceFactory = ({
relayHost, relayHost,
relayPort: Number(relayPort), relayPort: Number(relayPort),
tlsOptions: { tlsOptions: {
key: privateKey, key: privateKey.toString(),
ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(), ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
cert: clientCert.toString("pem") cert: clientCert.toString("pem")
}, },

View File

@ -100,6 +100,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"), tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer), tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"), tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
tx.ref("comment").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerComment"),
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"), tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"), tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"), tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
@ -162,8 +163,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
reviewerEmail: email, reviewerEmail: email,
reviewerLastName: lastName, reviewerLastName: lastName,
reviewerUsername: username, reviewerUsername: username,
reviewerFirstName: firstName reviewerFirstName: firstName,
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined) reviewerComment: comment
}) =>
userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined
}, },
{ {
key: "approverUserId", key: "approverUserId",

View File

@ -320,6 +320,7 @@ export const secretApprovalRequestServiceFactory = ({
approvalId, approvalId,
actor, actor,
status, status,
comment,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
@ -372,15 +373,18 @@ export const secretApprovalRequestServiceFactory = ({
return secretApprovalRequestReviewerDAL.create( return secretApprovalRequestReviewerDAL.create(
{ {
status, status,
comment,
requestId: secretApprovalRequest.id, requestId: secretApprovalRequest.id,
reviewerUserId: actorId reviewerUserId: actorId
}, },
tx tx
); );
} }
return secretApprovalRequestReviewerDAL.updateById(review.id, { status }, tx);
return secretApprovalRequestReviewerDAL.updateById(review.id, { status, comment }, tx);
}); });
return reviewStatus;
return { ...reviewStatus, projectId: secretApprovalRequest.projectId };
}; };
const updateApprovalStatus = async ({ const updateApprovalStatus = async ({
@ -499,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({
if (!hasMinApproval && !isSoftEnforcement) if (!hasMinApproval && !isSoftEnforcement)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
let mergeStatus; let mergeStatus;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
// this cycle if for bridged secrets // this cycle if for bridged secrets
@ -857,7 +861,6 @@ export const secretApprovalRequestServiceFactory = ({
if (isSoftEnforcement) { if (isSoftEnforcement) {
const cfg = getConfig(); const cfg = getConfig();
const project = await projectDAL.findProjectById(projectId);
const env = await projectEnvDAL.findOne({ id: policy.envId }); const env = await projectEnvDAL.findOne({ id: policy.envId });
const requestedByUser = await userDAL.findOne({ id: actorId }); const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
@ -1152,7 +1155,8 @@ export const secretApprovalRequestServiceFactory = ({
environment: env.name, environment: env.name,
secretPath, secretPath,
projectId, projectId,
requestId: secretApprovalRequest.id requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
} }
} }
}); });
@ -1452,7 +1456,8 @@ export const secretApprovalRequestServiceFactory = ({
environment: env.name, environment: env.name,
secretPath, secretPath,
projectId, projectId,
requestId: secretApprovalRequest.id requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
} }
} }
}); });

View File

@ -80,6 +80,7 @@ export type TStatusChangeDTO = {
export type TReviewRequestDTO = { export type TReviewRequestDTO = {
approvalId: string; approvalId: string;
status: ApprovalStatus; status: ApprovalStatus;
comment?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TApprovalRequestCountDTO = TProjectPermission; export type TApprovalRequestCountDTO = TProjectPermission;

View File

@ -13,6 +13,7 @@ import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@ -332,6 +333,7 @@ export const secretRotationQueueFactory = ({
await secretVersionV2BridgeDAL.insertMany( await secretVersionV2BridgeDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el, ...el,
actorType: ActorType.PLATFORM,
secretId: id secretId: id
})), })),
tx tx

View File

@ -7,6 +7,7 @@ import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { InternalServerError, NotFoundError } from "@app/lib/errors"; import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@ -370,7 +371,21 @@ export const secretSnapshotServiceFactory = ({
const secrets = await secretV2BridgeDAL.insertMany( const secrets = await secretV2BridgeDAL.insertMany(
rollbackSnaps.flatMap(({ secretVersions, folderId }) => rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
secretVersions.map( secretVersions.map(
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({ ({
latestSecretVersion,
version,
updatedAt,
createdAt,
secretId,
envId,
id,
tags,
// exclude the bottom fields from the secret - they are for versioning only.
userActorId,
identityActorId,
actorType,
...el
}) => ({
...el, ...el,
id: secretId, id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion, version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
@ -401,8 +416,18 @@ export const secretSnapshotServiceFactory = ({
})), })),
tx tx
); );
const userActorId = actor === ActorType.USER ? actorId : undefined;
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
const actorType = actor || ActorType.PLATFORM;
const secretVersions = await secretVersionV2BridgeDAL.insertMany( const secretVersions = await secretVersionV2BridgeDAL.insertMany(
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })), secrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el,
secretId: id,
userActorId,
identityActorId,
actorType
})),
tx tx
); );
await secretVersionV2TagBridgeDAL.insertMany( await secretVersionV2TagBridgeDAL.insertMany(

View File

@ -6,7 +6,6 @@ export const sanitizedSshCertificate = SshCertificatesSchema.pick({
sshCertificateTemplateId: true, sshCertificateTemplateId: true,
serialNumber: true, serialNumber: true,
certType: true, certType: true,
publicKey: true,
principals: true, principals: true,
keyId: true, keyId: true,
notBefore: true, notBefore: true,

View File

@ -638,7 +638,8 @@ export const FOLDERS = {
environment: "The slug of the environment to create the folder in.", environment: "The slug of the environment to create the folder in.",
name: "The name of the folder to create.", name: "The name of the folder to create.",
path: "The path of the folder to create.", path: "The path of the folder to create.",
directory: "The directory of the folder to create. (Deprecated in favor of path)" directory: "The directory of the folder to create. (Deprecated in favor of path)",
description: "An optional description label for the folder."
}, },
UPDATE: { UPDATE: {
folderId: "The ID of the folder to update.", folderId: "The ID of the folder to update.",
@ -647,7 +648,8 @@ export const FOLDERS = {
path: "The path of the folder to update.", path: "The path of the folder to update.",
directory: "The new directory of the folder to update. (Deprecated in favor of path)", directory: "The new directory of the folder to update. (Deprecated in favor of path)",
projectSlug: "The slug of the project where the folder is located.", projectSlug: "The slug of the project where the folder is located.",
workspaceId: "The ID of the project where the folder is located." workspaceId: "The ID of the project where the folder is located.",
description: "An optional description label for the folder."
}, },
DELETE: { DELETE: {
folderIdOrName: "The ID or name of the folder to delete.", folderIdOrName: "The ID or name of the folder to delete.",

View File

@ -1,6 +1,8 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import crypto from "node:crypto";
import net from "node:net"; import net from "node:net";
import tls from "node:tls";
import quicDefault, * as quicModule from "@infisical/quic";
import { BadRequestError } from "../errors"; import { BadRequestError } from "../errors";
import { logger } from "../logger"; import { logger } from "../logger";
@ -8,34 +10,73 @@ import { logger } from "../logger";
const DEFAULT_MAX_RETRIES = 3; const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RETRY_DELAY = 1000; // 1 second const DEFAULT_RETRY_DELAY = 1000; // 1 second
const createTLSConnection = (relayHost: string, relayPort: number, tlsOptions: tls.TlsOptions = {}) => { const quic = quicDefault || quicModule;
return new Promise<tls.TLSSocket>((resolve, reject) => {
// @ts-expect-error this is resolved in next connect
const socket = new tls.TLSSocket(null, {
rejectUnauthorized: true,
...tlsOptions
});
const cleanup = () => { const parseSubjectDetails = (data: string) => {
socket.removeAllListeners(); const values: Record<string, string> = {};
socket.end(); data.split("\n").forEach((el) => {
}; const [key, value] = el.split("=");
values[key.trim()] = value.trim();
socket.once("error", (err) => {
cleanup();
reject(err);
});
socket.connect(relayPort, relayHost, () => {
resolve(socket);
});
}); });
return values;
};
type TTlsOption = { ca: string; cert: string; key: string };
const createQuicConnection = async (
relayHost: string,
relayPort: number,
tlsOptions: TTlsOption,
identityId: string,
orgId: string
) => {
const client = await quic.QUICClient.createQUICClient({
host: relayHost,
port: relayPort,
config: {
ca: tlsOptions.ca,
cert: tlsOptions.cert,
key: tlsOptions.key,
applicationProtos: ["infisical-gateway"],
verifyPeer: true,
verifyCallback: async (certs) => {
if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired;
const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0]));
const caCertificate = new crypto.X509Certificate(tlsOptions.ca);
const isValidServerCertificate = serverCertificate.checkIssued(caCertificate);
if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate;
const subjectDetails = parseSubjectDetails(serverCertificate.subject);
if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) {
return quic.native.CryptoError.CertificateUnknown;
}
if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) {
return quic.native.CryptoError.CertificateExpired;
}
const formatedRelayHost =
process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost;
if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate;
},
maxIdleTimeout: 90000,
keepAliveIntervalTime: 30000
},
crypto: {
ops: {
randomBytes: async (data) => {
crypto.getRandomValues(new Uint8Array(data));
}
}
}
});
return client;
}; };
type TPingGatewayAndVerifyDTO = { type TPingGatewayAndVerifyDTO = {
relayHost: string; relayHost: string;
relayPort: number; relayPort: number;
tlsOptions: tls.TlsOptions; tlsOptions: TTlsOption;
maxRetries?: number; maxRetries?: number;
identityId: string; identityId: string;
orgId: string; orgId: string;
@ -44,56 +85,44 @@ type TPingGatewayAndVerifyDTO = {
export const pingGatewayAndVerify = async ({ export const pingGatewayAndVerify = async ({
relayHost, relayHost,
relayPort, relayPort,
tlsOptions = {}, tlsOptions,
maxRetries = DEFAULT_MAX_RETRIES, maxRetries = DEFAULT_MAX_RETRIES,
identityId, identityId,
orgId orgId
}: TPingGatewayAndVerifyDTO) => { }: TPingGatewayAndVerifyDTO) => {
let lastError: Error | null = null; let lastError: Error | null = null;
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
throw new BadRequestError({
error: err as Error
});
});
for (let attempt = 1; attempt <= maxRetries; attempt += 1) { for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
try { try {
const socket = await createTLSConnection(relayHost, relayPort, tlsOptions); const stream = quicClient.connection.newStream("bidi");
socket.setTimeout(2000); const pingWriter = stream.writable.getWriter();
await pingWriter.write(Buffer.from("PING\n"));
pingWriter.releaseLock();
const pingResult = await new Promise((resolve, reject) => { // Read PONG response
socket.once("timeout", () => { const reader = stream.readable.getReader();
socket.destroy(); const { value, done } = await reader.read();
reject(new Error("Timeout"));
if (done) {
throw new BadRequestError({
message: "Gateway closed before receiving PONG"
}); });
socket.once("close", () => { }
socket.destroy();
const response = Buffer.from(value).toString();
if (response !== "PONG\n" && response !== "PONG") {
throw new BadRequestError({
message: `Failed to Ping. Unexpected response: ${response}`
}); });
}
socket.once("end", () => { reader.releaseLock();
socket.destroy(); return;
});
socket.once("error", (err) => {
reject(err);
});
socket.write(Buffer.from("PING\n"), () => {
socket.once("data", (data) => {
const response = (data as string).toString();
const certificate = socket.getPeerCertificate();
if (certificate.subject.CN !== identityId || certificate.subject.O !== orgId) {
throw new BadRequestError({
message: `Invalid gateway. Certificate not found for ${identityId} in organization ${orgId}`
});
}
if (response === "PONG") {
resolve(true);
} else {
reject(new Error(`Unexpected response: ${response}`));
}
});
});
});
socket.end();
return pingResult;
} catch (err) { } catch (err) {
lastError = err as Error; lastError = err as Error;
@ -102,6 +131,8 @@ export const pingGatewayAndVerify = async ({
setTimeout(resolve, DEFAULT_RETRY_DELAY); setTimeout(resolve, DEFAULT_RETRY_DELAY);
}); });
} }
} finally {
await quicClient.destroy();
} }
} }
@ -114,76 +145,125 @@ export const pingGatewayAndVerify = async ({
interface TProxyServer { interface TProxyServer {
server: net.Server; server: net.Server;
port: number; port: number;
cleanup: () => void; cleanup: () => Promise<void>;
} }
const setupProxyServer = ({ const setupProxyServer = async ({
targetPort, targetPort,
targetHost, targetHost,
tlsOptions = {}, tlsOptions,
relayHost, relayHost,
relayPort relayPort,
identityId,
orgId
}: { }: {
targetHost: string; targetHost: string;
targetPort: number; targetPort: number;
relayPort: number; relayPort: number;
relayHost: string; relayHost: string;
tlsOptions: tls.TlsOptions; tlsOptions: TTlsOption;
identityId: string;
orgId: string;
}): Promise<TProxyServer> => { }): Promise<TProxyServer> => {
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
throw new BadRequestError({
error: err as Error
});
});
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const server = net.createServer(); const server = net.createServer();
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientSocket) => { server.on("connection", async (clientConn) => {
try { try {
const targetSocket = await createTLSConnection(relayHost, relayPort, tlsOptions); clientConn.setKeepAlive(true, 30000); // 30 seconds
clientConn.setNoDelay(true);
targetSocket.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`), () => { const stream = quicClient.connection.newStream("bidi");
clientSocket.on("data", (data) => { // Send FORWARD-TCP command
const flushed = targetSocket.write(data); const forwardWriter = stream.writable.getWriter();
if (!flushed) { await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
clientSocket.pause(); forwardWriter.releaseLock();
targetSocket.once("drain", () => { /* eslint-disable @typescript-eslint/no-misused-promises */
clientSocket.resume(); // Set up bidirectional copy
}); const setupCopy = async () => {
} // Client to QUIC
}); // eslint-disable-next-line
(async () => {
try {
const writer = stream.writable.getWriter();
targetSocket.on("data", (data) => { // Create a handler for client data
const flushed = clientSocket.write(data as string); clientConn.on("data", async (chunk) => {
if (!flushed) { await writer.write(chunk);
targetSocket.pause();
clientSocket.once("drain", () => {
targetSocket.resume();
}); });
// Handle client connection close
clientConn.on("end", async () => {
await writer.close();
});
clientConn.on("error", async (err) => {
await writer.abort(err);
});
} catch (err) {
clientConn.destroy();
} }
}); })();
// QUIC to Client
void (async () => {
try {
const reader = stream.readable.getReader();
let reading = true;
while (reading) {
const { value, done } = await reader.read();
if (done) {
reading = false;
clientConn.end(); // Close client connection when QUIC stream ends
break;
}
// Write data to TCP client
const canContinue = clientConn.write(Buffer.from(value));
// Handle backpressure
if (!canContinue) {
await new Promise((res) => {
clientConn.once("drain", res);
});
}
}
} catch (err) {
clientConn.destroy();
}
})();
};
await setupCopy();
//
// Handle connection closure
clientConn.on("close", async () => {
await stream.destroy();
}); });
const cleanup = () => { const cleanup = async () => {
clientSocket?.unpipe(); clientConn?.destroy();
clientSocket?.end(); await stream.destroy();
targetSocket?.unpipe();
targetSocket?.end();
}; };
clientSocket.on("error", (err) => { clientConn.on("error", (err) => {
logger.error(err, "Client socket error"); logger.error(err, "Client socket error");
cleanup(); void cleanup();
reject(err); reject(err);
}); });
targetSocket.on("error", (err) => { clientConn.on("end", cleanup);
logger.error(err, "Target socket error");
cleanup();
reject(err);
});
clientSocket.on("end", cleanup);
targetSocket.on("end", cleanup);
} catch (err) { } catch (err) {
logger.error(err, "Failed to establish target connection:"); logger.error(err, "Failed to establish target connection:");
clientSocket.end(); clientConn.end();
reject(err); reject(err);
} }
}); });
@ -192,6 +272,12 @@ const setupProxyServer = ({
reject(err); reject(err);
}); });
server.on("close", async () => {
await quicClient?.destroy();
});
/* eslint-enable */
server.listen(0, () => { server.listen(0, () => {
const address = server.address(); const address = server.address();
if (!address || typeof address === "string") { if (!address || typeof address === "string") {
@ -204,8 +290,9 @@ const setupProxyServer = ({
resolve({ resolve({
server, server,
port: address.port, port: address.port,
cleanup: () => { cleanup: async () => {
server.close(); server.close();
await quicClient?.destroy();
} }
}); });
}); });
@ -217,8 +304,7 @@ interface ProxyOptions {
targetPort: number; targetPort: number;
relayHost: string; relayHost: string;
relayPort: number; relayPort: number;
tlsOptions?: tls.TlsOptions; tlsOptions: TTlsOption;
maxRetries?: number;
identityId: string; identityId: string;
orgId: string; orgId: string;
} }
@ -227,30 +313,19 @@ export const withGatewayProxy = async (
callback: (port: number) => Promise<void>, callback: (port: number) => Promise<void>,
options: ProxyOptions options: ProxyOptions
): Promise<void> => { ): Promise<void> => {
const { const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
relayHost,
relayPort, // Setup the proxy server
const { port, cleanup } = await setupProxyServer({
targetHost, targetHost,
targetPort, targetPort,
tlsOptions = {},
maxRetries = DEFAULT_MAX_RETRIES,
identityId,
orgId
} = options;
// First, try to ping the gateway
await pingGatewayAndVerify({
relayHost,
relayPort, relayPort,
relayHost,
tlsOptions, tlsOptions,
maxRetries,
identityId, identityId,
orgId orgId
}); });
// Setup the proxy server
const { port, cleanup } = await setupProxyServer({ targetHost, targetPort, relayPort, relayHost, tlsOptions });
try { try {
// Execute the callback with the allocated port // Execute the callback with the allocated port
await callback(port); await callback(port);
@ -259,6 +334,6 @@ export const withGatewayProxy = async (
throw new BadRequestError({ message: (err as Error)?.message }); throw new BadRequestError({ message: (err as Error)?.message });
} finally { } finally {
// Ensure cleanup happens regardless of success or failure // Ensure cleanup happens regardless of success or failure
cleanup(); await cleanup();
} }
}; };

View File

@ -1,8 +1,8 @@
import opentelemetry, { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api"; import opentelemetry, { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus"; import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import { registerInstrumentations } from "@opentelemetry/instrumentation"; import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { Resource } from "@opentelemetry/resources"; import { Resource } from "@opentelemetry/resources";
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
@ -70,7 +70,7 @@ const initTelemetryInstrumentation = ({
opentelemetry.metrics.setGlobalMeterProvider(meterProvider); opentelemetry.metrics.setGlobalMeterProvider(meterProvider);
registerInstrumentations({ registerInstrumentations({
instrumentations: [getNodeAutoInstrumentations()] instrumentations: [new HttpInstrumentation()]
}); });
}; };

View File

@ -83,6 +83,14 @@ const run = async () => {
process.exit(0); process.exit(0);
}); });
process.on("uncaughtException", (error) => {
logger.error(error, "CRITICAL ERROR: Uncaught Exception");
});
process.on("unhandledRejection", (error) => {
logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection");
});
await server.listen({ await server.listen({
port: envConfig.PORT, port: envConfig.PORT,
host: envConfig.HOST, host: envConfig.HOST,

View File

@ -21,6 +21,7 @@ import {
TQueueSecretSyncSyncSecretsByIdDTO, TQueueSecretSyncSyncSecretsByIdDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO TQueueSendSecretSyncActionFailedNotificationsDTO
} from "@app/services/secret-sync/secret-sync-types"; } from "@app/services/secret-sync/secret-sync-types";
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
export enum QueueName { export enum QueueName {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
@ -107,7 +108,7 @@ export type TQueueJobTypes = {
}; };
[QueueName.SecretWebhook]: { [QueueName.SecretWebhook]: {
name: QueueJobs.SecWebhook; name: QueueJobs.SecWebhook;
payload: { projectId: string; environment: string; secretPath: string; depth?: number }; payload: TWebhookPayloads;
}; };
[QueueName.AccessTokenStatusUpdate]: [QueueName.AccessTokenStatusUpdate]:

View File

@ -1096,7 +1096,9 @@ export const registerRoutes = async (
permissionService, permissionService,
secretSharingDAL, secretSharingDAL,
orgDAL, orgDAL,
kmsService kmsService,
smtpService,
userDAL
}); });
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({

View File

@ -111,7 +111,16 @@ export const secretRawSchema = z.object({
secretReminderRepeatDays: z.number().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
actor: z
.object({
actorId: z.string().nullable().optional(),
actorType: z.string().nullable().optional(),
name: z.string().nullable().optional(),
membershipId: z.string().nullable().optional()
})
.optional()
.nullable()
}); });
export const ProjectPermissionSchema = z.object({ export const ProjectPermissionSchema = z.object({

View File

@ -211,6 +211,27 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "PATCH",
url: "/user-management/users/:userId/admin-access",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
userId: z.string()
})
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
await server.services.superAdmin.grantServerAdminAccessToUser(req.params.userId);
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/encryption-strategies", url: "/encryption-strategies",

View File

@ -37,6 +37,7 @@ import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router"; import { registerProjectRouter } from "./project-router";
import { registerSecretFolderRouter } from "./secret-folder-router"; import { registerSecretFolderRouter } from "./secret-folder-router";
import { registerSecretImportRouter } from "./secret-import-router"; import { registerSecretImportRouter } from "./secret-import-router";
import { registerSecretRequestsRouter } from "./secret-requests-router";
import { registerSecretSharingRouter } from "./secret-sharing-router"; import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router"; import { registerSecretTagRouter } from "./secret-tag-router";
import { registerSlackRouter } from "./slack-router"; import { registerSlackRouter } from "./slack-router";
@ -110,7 +111,15 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" }); await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
await server.register(registerWebhookRouter, { prefix: "/webhooks" }); await server.register(registerWebhookRouter, { prefix: "/webhooks" });
await server.register(registerIdentityRouter, { prefix: "/identities" }); await server.register(registerIdentityRouter, { prefix: "/identities" });
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
await server.register(
async (secretSharingRouter) => {
await secretSharingRouter.register(registerSecretSharingRouter, { prefix: "/shared" });
await secretSharingRouter.register(registerSecretRequestsRouter, { prefix: "/requests" });
},
{ prefix: "/secret-sharing" }
);
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
await server.register(registerDashboardRouter, { prefix: "/dashboard" }); await server.register(registerDashboardRouter, { prefix: "/dashboard" });
await server.register(registerCmekRouter, { prefix: "/kms" }); await server.register(registerCmekRouter, { prefix: "/kms" });

View File

@ -47,7 +47,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash)
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.directory) .describe(FOLDERS.CREATE.directory),
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -65,7 +66,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
path path,
description: req.body.description
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@ -76,7 +78,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
environment: req.body.environment, environment: req.body.environment,
folderId: folder.id, folderId: folder.id,
folderName: folder.name, folderName: folder.name,
folderPath: path folderPath: path,
...(req.body.description ? { description: req.body.description } : {})
} }
} }
}); });
@ -125,7 +128,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash)
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.directory) .describe(FOLDERS.UPDATE.directory),
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -196,7 +200,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash)
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path) .describe(FOLDERS.UPDATE.path),
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
}) })
.array() .array()
.min(1) .min(1)

View File

@ -0,0 +1,270 @@
import { z } from "zod";
import { SecretSharingSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSharingAccessType } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerSecretRequestsRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
}).extend({
isSecretValueSet: z.boolean(),
requester: z.object({
organizationName: z.string(),
firstName: z.string().nullish(),
lastName: z.string().nullish(),
username: z.string()
})
})
})
}
},
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.getSecretRequestById({
id: req.params.id,
actorOrgId: req.permission?.orgId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod
});
return { secretRequest };
}
});
server.route({
method: "POST",
url: "/:id/set-value",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
secretValue: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
})
})
}
},
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({
id: req.params.id,
actorOrgId: req.permission?.orgId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod,
secretValue: req.body.secretValue
});
return { secretRequest };
}
});
server.route({
method: "POST",
url: "/:id/reveal-value",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
}).extend({
secretValue: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.revealSecretRequestValue({
id: req.params.id,
actorOrgId: req.permission.orgId,
orgId: req.permission.orgId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod
});
return { secretRequest };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.deleteSharedSecretById({
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
sharedSecretId: req.params.id,
orgId: req.permission.orgId,
actor: req.permission.type,
type: SecretSharingType.Request
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretRequestDeleted,
distinctId: getTelemetryDistinctId(req),
properties: {
secretRequestId: req.params.id,
organizationId: req.permission.orgId,
...req.auditLogInfo
}
});
return { secretRequest };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0),
limit: z.coerce.number().min(1).max(100).default(25)
}),
response: {
200: z.object({
secrets: z.array(SecretSharingSchema),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
type: SecretSharingType.Request,
...req.query
});
return {
secrets,
totalCount
};
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
name: z.string().max(50).optional(),
expiresAt: z.string(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
}),
response: {
200: z.object({
id: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const shareRequest = await req.server.services.secretSharing.createSecretRequest({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SECRET_REQUEST,
metadata: {
accessType: req.body.accessType,
name: req.body.name,
id: shareRequest.id
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretRequestCreated,
distinctId: getTelemetryDistinctId(req),
properties: {
secretRequestId: shareRequest.id,
organizationId: req.permission.orgId,
secretRequestName: req.body.name,
...req.auditLogInfo
}
});
return { id: shareRequest.id };
}
});
};

View File

@ -11,6 +11,7 @@ import {
} from "@app/server/config/rateLimiter"; } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => { export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@ -38,6 +39,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
type: SecretSharingType.Share,
...req.query ...req.query
}); });
@ -211,7 +213,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
orgId: req.permission.orgId, orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
sharedSecretId sharedSecretId,
type: SecretSharingType.Share
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({

View File

@ -380,6 +380,48 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/raw/id/:secretId",
config: {
rateLimit: secretsLimit
},
schema: {
params: z.object({
secretId: z.string()
}),
response: {
200: z.object({
secret: secretRawSchema.extend({
secretPath: z.string(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional(),
secretMetadata: ResourceMetadataSchema.optional()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { secretId } = req.params;
const secret = await server.services.secret.getSecretByIdRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretId
});
return { secret };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/raw/:secretName", url: "/raw/:secretName",

View File

@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx tx
}); });
} }

View File

@ -134,7 +134,15 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
* Return list of names of apps for Vercel integration * Return list of names of apps for Vercel integration
* This is re-used for getting custom environments for Vercel * This is re-used for getting custom environments for Vercel
*/ */
export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => { export const getAppsVercel = async ({
accessToken,
teamId,
includeCustomEnvironments
}: {
teamId?: string | null;
accessToken: string;
includeCustomEnvironments?: boolean;
}) => {
const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = []; const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = [];
const limit = "20"; const limit = "20";
@ -145,12 +153,6 @@ export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string |
projects: { projects: {
name: string; name: string;
id: string; id: string;
customEnvironments?: {
id: string;
type: string;
description: string;
slug: string;
}[];
}[]; }[];
pagination: { pagination: {
count: number; count: number;
@ -159,6 +161,20 @@ export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string |
}; };
} }
const getProjectCustomEnvironments = async (projectId: string) => {
const { data } = await request.get<{ environments: { id: string; slug: string }[] }>(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${projectId}/custom-environments`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
return data.environments;
};
while (hasMorePages) { while (hasMorePages) {
const params: { [key: string]: string } = { const params: { [key: string]: string } = {
limit limit
@ -180,17 +196,38 @@ export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string |
} }
}); });
data.projects.forEach((a) => { if (includeCustomEnvironments) {
apps.push({ const projectsWithCustomEnvironments = await Promise.all(
name: a.name, data.projects.map(async (a) => {
appId: a.id, const customEnvironments = await getProjectCustomEnvironments(a.id);
customEnvironments:
a.customEnvironments?.map((env) => ({ return {
slug: env.slug, ...a,
id: env.id customEnvironments
})) ?? [] };
})
);
projectsWithCustomEnvironments.forEach((a) => {
apps.push({
name: a.name,
appId: a.id,
customEnvironments:
a.customEnvironments?.map((env) => ({
slug: env.slug,
id: env.id
})) ?? []
});
}); });
}); } else {
data.projects.forEach((a) => {
apps.push({
name: a.name,
appId: a.id,
customEnvironments: []
});
});
}
next = data.pagination.next; next = data.pagination.next;

View File

@ -114,20 +114,27 @@ export const integrationAuthServiceFactory = ({
const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => { const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => {
const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string); const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string);
return Promise.all( const filteredAuthorizations = await Promise.all(
authorizations.filter(async (auth) => { authorizations.map(async (auth) => {
const { permission } = await permissionService.getProjectPermission({ try {
actor, const { permission } = await permissionService.getProjectPermission({
actorId, actor,
projectId: auth.projectId, actorId,
actorAuthMethod, projectId: auth.projectId,
actorOrgId, actorAuthMethod,
actionProjectType: ActionProjectType.SecretManager actorOrgId,
}); actionProjectType: ActionProjectType.SecretManager
});
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations) ? auth : null;
} catch (error) {
// user does not belong to the project that the integration auth belongs to
return null;
}
}) })
); );
return filteredAuthorizations.filter((auth): auth is NonNullable<typeof auth> => auth !== null);
}; };
const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => { const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {
@ -1851,6 +1858,7 @@ export const integrationAuthServiceFactory = ({
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const vercelApps = await getAppsVercel({ const vercelApps = await getAppsVercel({
includeCustomEnvironments: true,
accessToken, accessToken,
teamId teamId
}); });

View File

@ -20,7 +20,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">; secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">; secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">; snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">; secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
}; };
@ -45,6 +45,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await identityAccessTokenDAL.removeExpiredTokens(); await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets(); await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets();
await secretSharingDAL.pruneExpiredSecretRequests();
await snapshotDAL.pruneExcessSnapshots(); await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions(); await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions();

View File

@ -50,7 +50,8 @@ export const secretFolderServiceFactory = ({
actorOrgId, actorOrgId,
name, name,
environment, environment,
path: secretPath path: secretPath,
description
}: TCreateFolderDTO) => { }: TCreateFolderDTO) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@ -121,7 +122,10 @@ export const secretFolderServiceFactory = ({
} }
} }
const doc = await folderDAL.create({ name, envId: env.id, version: 1, parentId: parentFolderId }, tx); const doc = await folderDAL.create(
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
tx
);
await folderVersionDAL.create( await folderVersionDAL.create(
{ {
name: doc.name, name: doc.name,
@ -170,7 +174,7 @@ export const secretFolderServiceFactory = ({
const result = await folderDAL.transaction(async (tx) => const result = await folderDAL.transaction(async (tx) =>
Promise.all( Promise.all(
folders.map(async (newFolder) => { folders.map(async (newFolder) => {
const { environment, path: secretPath, id, name } = newFolder; const { environment, path: secretPath, id, name, description } = newFolder;
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath); const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!parentFolder) { if (!parentFolder) {
@ -217,7 +221,7 @@ export const secretFolderServiceFactory = ({
const [doc] = await folderDAL.update( const [doc] = await folderDAL.update(
{ envId: env.id, id: folder.id, parentId: parentFolder.id }, { envId: env.id, id: folder.id, parentId: parentFolder.id },
{ name }, { name, description },
tx tx
); );
await folderVersionDAL.create( await folderVersionDAL.create(
@ -259,7 +263,8 @@ export const secretFolderServiceFactory = ({
name, name,
environment, environment,
path: secretPath, path: secretPath,
id id,
description
}: TUpdateFolderDTO) => { }: TUpdateFolderDTO) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@ -312,7 +317,7 @@ export const secretFolderServiceFactory = ({
const newFolder = await folderDAL.transaction(async (tx) => { const newFolder = await folderDAL.transaction(async (tx) => {
const [doc] = await folderDAL.update( const [doc] = await folderDAL.update(
{ envId: env.id, id: folder.id, parentId: parentFolder.id, isReserved: false }, { envId: env.id, id: folder.id, parentId: parentFolder.id, isReserved: false },
{ name }, { name, description },
tx tx
); );
await folderVersionDAL.create( await folderVersionDAL.create(

View File

@ -9,6 +9,7 @@ export type TCreateFolderDTO = {
environment: string; environment: string;
path: string; path: string;
name: string; name: string;
description?: string | null;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateFolderDTO = { export type TUpdateFolderDTO = {
@ -16,6 +17,7 @@ export type TUpdateFolderDTO = {
path: string; path: string;
id: string; id: string;
name: string; name: string;
description?: string | null;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateManyFoldersDTO = { export type TUpdateManyFoldersDTO = {
@ -25,6 +27,7 @@ export type TUpdateManyFoldersDTO = {
path: string; path: string;
id: string; id: string;
name: string; name: string;
description?: string | null;
}[]; }[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@ -2,17 +2,61 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretSharing } from "@app/db/schemas"; import { TableName, TSecretSharing } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
import { SecretSharingType } from "./secret-sharing-types";
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>; export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
export const secretSharingDALFactory = (db: TDbClient) => { export const secretSharingDALFactory = (db: TDbClient) => {
const sharedSecretOrm = ormify(db, TableName.SecretSharing); const sharedSecretOrm = ormify(db, TableName.SecretSharing);
const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => { const getSecretRequestById = async (id: string) => {
const repDb = db.replicaNode();
const secretRequest = await repDb(TableName.SecretSharing)
.leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.SecretSharing}.orgId`)
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretSharing}.userId`)
.where(`${TableName.SecretSharing}.id`, id)
.where(`${TableName.SecretSharing}.type`, SecretSharingType.Request)
.select(
repDb.ref("name").withSchema(TableName.Organization).as("orgName"),
repDb.ref("firstName").withSchema(TableName.Users).as("requesterFirstName"),
repDb.ref("lastName").withSchema(TableName.Users).as("requesterLastName"),
repDb.ref("username").withSchema(TableName.Users).as("requesterUsername")
)
.select(selectAllTableCols(TableName.SecretSharing))
.first();
if (!secretRequest) {
throw new NotFoundError({
message: `Secret request with ID '${id}' not found`
});
}
return {
...secretRequest,
requester: {
organizationName: secretRequest.orgName,
firstName: secretRequest.requesterFirstName,
lastName: secretRequest.requesterLastName,
username: secretRequest.requesterUsername
}
};
};
const countAllUserOrgSharedSecrets = async ({
orgId,
userId,
type
}: {
orgId: string;
userId: string;
type: SecretSharingType;
}) => {
try { try {
interface CountResult { interface CountResult {
count: string; count: string;
@ -22,6 +66,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
.replicaNode()(TableName.SecretSharing) .replicaNode()(TableName.SecretSharing)
.where(`${TableName.SecretSharing}.orgId`, orgId) .where(`${TableName.SecretSharing}.orgId`, orgId)
.where(`${TableName.SecretSharing}.userId`, userId) .where(`${TableName.SecretSharing}.userId`, userId)
.where(`${TableName.SecretSharing}.type`, type)
.count("*") .count("*")
.first(); .first();
@ -38,6 +83,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
const docs = await (tx || db)(TableName.SecretSharing) const docs = await (tx || db)(TableName.SecretSharing)
.where("expiresAt", "<", today) .where("expiresAt", "<", today)
.andWhere("encryptedValue", "<>", "") .andWhere("encryptedValue", "<>", "")
.andWhere("type", SecretSharingType.Share)
.update({ .update({
encryptedValue: "", encryptedValue: "",
tag: "", tag: "",
@ -50,6 +96,26 @@ export const secretSharingDALFactory = (db: TDbClient) => {
} }
}; };
const pruneExpiredSecretRequests = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests started`);
try {
const today = new Date();
const docs = await (tx || db)(TableName.SecretSharing)
.whereNotNull("expiresAt")
.andWhere("expiresAt", "<", today)
.andWhere("encryptedSecret", null)
.andWhere("type", SecretSharingType.Request)
.delete();
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests completed`);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "pruneExpiredSecretRequests" });
}
};
const findActiveSharedSecrets = async (filters: Partial<TSecretSharing>, tx?: Knex) => { const findActiveSharedSecrets = async (filters: Partial<TSecretSharing>, tx?: Knex) => {
try { try {
const now = new Date(); const now = new Date();
@ -57,6 +123,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
.where(filters) .where(filters)
.andWhere("expiresAt", ">", now) .andWhere("expiresAt", ">", now)
.andWhere("encryptedValue", "<>", "") .andWhere("encryptedValue", "<>", "")
.andWhere("type", SecretSharingType.Share)
.select(selectAllTableCols(TableName.SecretSharing)) .select(selectAllTableCols(TableName.SecretSharing))
.orderBy("expiresAt", "asc"); .orderBy("expiresAt", "asc");
} catch (error) { } catch (error) {
@ -86,7 +153,9 @@ export const secretSharingDALFactory = (db: TDbClient) => {
...sharedSecretOrm, ...sharedSecretOrm,
countAllUserOrgSharedSecrets, countAllUserOrgSharedSecrets,
pruneExpiredSharedSecrets, pruneExpiredSharedSecrets,
pruneExpiredSecretRequests,
softDeleteById, softDeleteById,
findActiveSharedSecrets findActiveSharedSecrets,
getSecretRequestById
}; };
}; };

View File

@ -4,26 +4,36 @@ import bcrypt from "bcrypt";
import { TSecretSharing } from "@app/db/schemas"; import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types"; import { SecretSharingAccessType } from "@app/lib/types";
import { isUuidV4 } from "@app/lib/validator"; import { isUuidV4 } from "@app/lib/validator";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal";
import { import {
SecretSharingType,
TCreatePublicSharedSecretDTO, TCreatePublicSharedSecretDTO,
TCreateSecretRequestDTO,
TCreateSharedSecretDTO, TCreateSharedSecretDTO,
TDeleteSharedSecretDTO, TDeleteSharedSecretDTO,
TGetActiveSharedSecretByIdDTO, TGetActiveSharedSecretByIdDTO,
TGetSharedSecretsDTO TGetSecretRequestByIdDTO,
TGetSharedSecretsDTO,
TRevealSecretRequestValueDTO,
TSetSecretRequestValueDTO
} from "./secret-sharing-types"; } from "./secret-sharing-types";
type TSecretSharingServiceFactoryDep = { type TSecretSharingServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
secretSharingDAL: TSecretSharingDALFactory; secretSharingDAL: TSecretSharingDALFactory;
orgDAL: TOrgDALFactory; orgDAL: TOrgDALFactory;
userDAL: TUserDALFactory;
kmsService: TKmsServiceFactory; kmsService: TKmsServiceFactory;
smtpService: TSmtpService;
}; };
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>; export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
@ -32,7 +42,9 @@ export const secretSharingServiceFactory = ({
permissionService, permissionService,
secretSharingDAL, secretSharingDAL,
orgDAL, orgDAL,
kmsService kmsService,
smtpService,
userDAL
}: TSecretSharingServiceFactoryDep) => { }: TSecretSharingServiceFactoryDep) => {
const $validateSharedSecretExpiry = (expiresAt: string) => { const $validateSharedSecretExpiry = (expiresAt: string) => {
if (new Date(expiresAt) < new Date()) { if (new Date(expiresAt) < new Date()) {
@ -75,7 +87,6 @@ export const secretSharingServiceFactory = ({
} }
const encryptWithRoot = kmsService.encryptWithRootKey(); const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue)); const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const id = crypto.randomBytes(32).toString("hex"); const id = crypto.randomBytes(32).toString("hex");
@ -88,6 +99,7 @@ export const secretSharingServiceFactory = ({
encryptedValue: null, encryptedValue: null,
encryptedSecret, encryptedSecret,
name, name,
type: SecretSharingType.Share,
password: hashedPassword, password: hashedPassword,
expiresAt: new Date(expiresAt), expiresAt: new Date(expiresAt),
expiresAfterViews, expiresAfterViews,
@ -101,6 +113,191 @@ export const secretSharingServiceFactory = ({
return { id: idToReturn }; return { id: idToReturn };
}; };
const createSecretRequest = async ({
actor,
accessType,
expiresAt,
name,
actorId,
orgId,
actorAuthMethod,
actorOrgId
}: TCreateSecretRequestDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
$validateSharedSecretExpiry(expiresAt);
const newSecretRequest = await secretSharingDAL.create({
type: SecretSharingType.Request,
userId: actorId,
orgId,
name,
encryptedSecret: null,
accessType,
expiresAt: new Date(expiresAt)
});
return { id: newSecretRequest.id };
};
const revealSecretRequestValue = async ({
id,
actor,
actorId,
actorOrgId,
orgId,
actorAuthMethod
}: TRevealSecretRequestValueDTO) => {
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) {
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
}
if (!secretRequest.encryptedSecret) {
throw new BadRequestError({ message: "Secret request has no value set" });
}
const decryptWithRoot = kmsService.decryptWithRootKey();
const decryptedSecret = decryptWithRoot(secretRequest.encryptedSecret);
return { ...secretRequest, secretValue: decryptedSecret.toString() };
};
const getSecretRequestById = async ({
id,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TGetSecretRequestByIdDTO) => {
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
if (!secretRequest.orgId) {
throw new BadRequestError({ message: "No organization ID present on secret request" });
}
if (!actorOrgId) {
throw new UnauthorizedError();
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
secretRequest.orgId,
actorAuthMethod,
actorOrgId
);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
}
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
throw new ForbiddenRequestError({
message: "Access denied: Secret request has expired"
});
}
return {
...secretRequest,
isSecretValueSet: Boolean(secretRequest.encryptedSecret)
};
};
const setSecretRequestValue = async ({
id,
actor,
actorId,
actorAuthMethod,
actorOrgId,
secretValue
}: TSetSecretRequestValueDTO) => {
const appCfg = getConfig();
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
let respondentUsername: string | undefined;
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
if (!secretRequest.orgId) {
throw new BadRequestError({ message: "No organization ID present on secret request" });
}
if (!actorOrgId) {
throw new UnauthorizedError();
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
secretRequest.orgId,
actorAuthMethod,
actorOrgId
);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
const user = await userDAL.findById(actorId);
if (!user) {
throw new NotFoundError({ message: `User with ID '${actorId}' not found` });
}
respondentUsername = user.username;
}
if (secretRequest.encryptedSecret) {
throw new BadRequestError({ message: "Secret request already has a value set" });
}
if (secretValue.length > 10_000) {
throw new BadRequestError({ message: "Shared secret value too long" });
}
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
throw new ForbiddenRequestError({
message: "Access denied: Secret request has expired"
});
}
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const request = await secretSharingDAL.transaction(async (tx) => {
const updatedRequest = await secretSharingDAL.updateById(id, { encryptedSecret }, tx);
await smtpService.sendMail({
recipients: [secretRequest.requesterUsername],
subjectLine: "Secret Request Completed",
substitutions: {
name: secretRequest.name,
respondentUsername,
secretRequestUrl: `${appCfg.SITE_URL}/organization/secret-sharing?selectedTab=request-secret`
},
template: SmtpTemplates.SecretRequestCompleted
});
return updatedRequest;
});
return request;
};
const createPublicSharedSecret = async ({ const createPublicSharedSecret = async ({
password, password,
secretValue, secretValue,
@ -121,6 +318,7 @@ export const secretSharingServiceFactory = ({
encryptedValue: null, encryptedValue: null,
iv: null, iv: null,
tag: null, tag: null,
type: SecretSharingType.Share,
encryptedSecret, encryptedSecret,
password: hashedPassword, password: hashedPassword,
expiresAt: new Date(expiresAt), expiresAt: new Date(expiresAt),
@ -137,7 +335,8 @@ export const secretSharingServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
offset, offset,
limit limit,
type
}: TGetSharedSecretsDTO) => { }: TGetSharedSecretsDTO) => {
if (!actorOrgId) throw new ForbiddenRequestError(); if (!actorOrgId) throw new ForbiddenRequestError();
@ -153,14 +352,16 @@ export const secretSharingServiceFactory = ({
const secrets = await secretSharingDAL.find( const secrets = await secretSharingDAL.find(
{ {
userId: actorId, userId: actorId,
orgId: actorOrgId orgId: actorOrgId,
type
}, },
{ offset, limit, sort: [["createdAt", "desc"]] } { offset, limit, sort: [["createdAt", "desc"]] }
); );
const count = await secretSharingDAL.countAllUserOrgSharedSecrets({ const count = await secretSharingDAL.countAllUserOrgSharedSecrets({
orgId: actorOrgId, orgId: actorOrgId,
userId: actorId userId: actorId,
type
}); });
return { return {
@ -187,9 +388,11 @@ export const secretSharingServiceFactory = ({
const sharedSecret = isUuidV4(sharedSecretId) const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findOne({ ? await secretSharingDAL.findOne({
id: sharedSecretId, id: sharedSecretId,
type: SecretSharingType.Share,
hashedHex hashedHex
}) })
: await secretSharingDAL.findOne({ : await secretSharingDAL.findOne({
type: SecretSharingType.Share,
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex") identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
}); });
@ -254,7 +457,7 @@ export const secretSharingServiceFactory = ({
secret: { secret: {
...sharedSecret, ...sharedSecret,
...(decryptedSecretValue && { ...(decryptedSecretValue && {
secretValue: Buffer.from(decryptedSecretValue).toString() secretValue: decryptedSecretValue.toString()
}), }),
orgName: orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
@ -270,11 +473,17 @@ export const secretSharingServiceFactory = ({
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
const sharedSecret = isUuidV4(sharedSecretId) const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findById(sharedSecretId) ? await secretSharingDAL.findOne({ id: sharedSecretId, type: deleteSharedSecretInput.type })
: await secretSharingDAL.findOne({ identifier: sharedSecretId }); : await secretSharingDAL.findOne({ identifier: sharedSecretId, type: deleteSharedSecretInput.type });
if (sharedSecret.orgId && sharedSecret.orgId !== orgId) if (sharedSecret.userId !== actorId) {
throw new ForbiddenRequestError({
message: "User does not have permission to delete shared secret"
});
}
if (sharedSecret.orgId && sharedSecret.orgId !== orgId) {
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" }); throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
}
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId); const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
@ -286,6 +495,11 @@ export const secretSharingServiceFactory = ({
createPublicSharedSecret, createPublicSharedSecret,
getSharedSecrets, getSharedSecrets,
deleteSharedSecretById, deleteSharedSecretById,
getSharedSecretById getSharedSecretById,
createSecretRequest,
getSecretRequestById,
setSecretRequestValue,
revealSecretRequestValue
}; };
}; };

View File

@ -1,8 +1,14 @@
import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types"; import { SecretSharingAccessType, TGenericPermission, TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export enum SecretSharingType {
Share = "share",
Request = "request"
}
export type TGetSharedSecretsDTO = { export type TGetSharedSecretsDTO = {
type: SecretSharingType;
offset: number; offset: number;
limit: number; limit: number;
} & TGenericPermission; } & TGenericPermission;
@ -39,6 +45,26 @@ export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
export type TCreateSecretRequestDTO = {
name?: string;
accessType: SecretSharingAccessType;
expiresAt: string;
} & TOrgPermission;
export type TRevealSecretRequestValueDTO = {
id: string;
} & TOrgPermission;
export type TGetSecretRequestByIdDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;
export type TSetSecretRequestValueDTO = {
id: string;
secretValue: string;
} & Omit<TOrgPermission, "orgId">;
export type TDeleteSharedSecretDTO = { export type TDeleteSharedSecretDTO = {
sharedSecretId: string; sharedSecretId: string;
type: SecretSharingType;
} & TSharedSecretPermission; } & TSharedSecretPermission;

View File

@ -613,6 +613,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id` `${TableName.SecretTag}.id`
) )
.leftJoin(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretV2)) .select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
@ -622,12 +625,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
); )
.select(db.ref("projectId").withSchema(TableName.Environment).as("projectId"));
const docs = sqlNestRelationships({ const docs = sqlNestRelationships({
data: rawDocs, data: rawDocs,
key: "id", key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), parentMapper: (el) => ({ _id: el.id, projectId: el.projectId, ...SecretsV2Schema.parse(el) }),
childrenMapper: [ childrenMapper: [
{ {
key: "tagId", key: "tagId",

View File

@ -5,6 +5,7 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
@ -62,6 +63,7 @@ export const fnSecretBulkInsert = async ({
resourceMetadataDAL, resourceMetadataDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor,
tx tx
}: TFnSecretBulkInsert) => { }: TFnSecretBulkInsert) => {
const sanitizedInputSecrets = inputSecrets.map( const sanitizedInputSecrets = inputSecrets.map(
@ -90,6 +92,10 @@ export const fnSecretBulkInsert = async ({
}) })
); );
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined;
const actorType = actor?.type || ActorType.PLATFORM;
const newSecrets = await secretDAL.insertMany( const newSecrets = await secretDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ ...el, folderId })), sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
tx tx
@ -106,6 +112,9 @@ export const fnSecretBulkInsert = async ({
sanitizedInputSecrets.map((el) => ({ sanitizedInputSecrets.map((el) => ({
...el, ...el,
folderId, folderId,
userActorId,
identityActorId,
actorType,
secretId: newSecretGroupedByKeyName[el.key][0].id secretId: newSecretGroupedByKeyName[el.key][0].id
})), })),
tx tx
@ -157,8 +166,13 @@ export const fnSecretBulkUpdate = async ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
resourceMetadataDAL resourceMetadataDAL,
actor
}: TFnSecretBulkUpdate) => { }: TFnSecretBulkUpdate) => {
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined;
const actorType = actor?.type || ActorType.PLATFORM;
const sanitizedInputSecrets = inputSecrets.map( const sanitizedInputSecrets = inputSecrets.map(
({ ({
filter, filter,
@ -216,7 +230,10 @@ export const fnSecretBulkUpdate = async ({
encryptedValue, encryptedValue,
reminderRepeatDays, reminderRepeatDays,
folderId, folderId,
secretId secretId,
userActorId,
identityActorId,
actorType
}) })
), ),
tx tx
@ -616,6 +633,12 @@ export const reshapeBridgeSecret = (
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & { secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
value: string; value: string;
comment: string; comment: string;
userActorName?: string | null;
identityActorName?: string | null;
userActorId?: string | null;
identityActorId?: string | null;
membershipId?: string | null;
actorType?: string | null;
tags?: { tags?: {
id: string; id: string;
slug: string; slug: string;
@ -636,6 +659,14 @@ export const reshapeBridgeSecret = (
_id: secret.id, _id: secret.id,
id: secret.id, id: secret.id,
user: secret.userId, user: secret.userId,
actor: secret.actorType
? {
actorType: secret.actorType,
actorId: secret.userActorId || secret.identityActorId,
name: secret.identityActorName || secret.userActorName,
membershipId: secret.membershipId
}
: undefined,
tags: secret.tags, tags: secret.tags,
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.reminderRepeatDays, secretReminderRepeatDays: secret.reminderRepeatDays,

View File

@ -28,6 +28,7 @@ import { KmsDataKey } from "../kms/kms-types";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretQueueFactory } from "../secret/secret-queue";
import { TGetASecretByIdDTO } from "../secret/secret-types";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
@ -73,7 +74,13 @@ type TSecretV2BridgeServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" | "findBySecretPath"
| "updateById"
| "findById"
| "findByManySecretPath"
| "find"
| "findBySecretPathMultiEnv"
| "findSecretPathByFolderIds"
>; >;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
@ -301,6 +308,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx tx
}) })
); );
@ -483,6 +494,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx tx
}) })
); );
@ -947,6 +962,73 @@ export const secretV2BridgeServiceFactory = ({
}; };
}; };
const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => {
const secret = await secretDAL.findOneWithTags({
[`${TableName.SecretV2}.id` as "id"]: secretId
});
if (!secret) {
throw new NotFoundError({
message: `Secret with ID '${secretId}' not found`,
name: "GetSecretById"
});
}
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secret.projectId, [secret.folderId]);
if (!folderWithPath) {
throw new NotFoundError({
message: `Folder with id '${secret.folderId}' not found`,
name: "GetSecretById"
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: secret.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: folderWithPath.environmentSlug,
secretPath: folderWithPath.path,
secretName: secret.key,
secretTags: secret.tags.map((i) => i.slug)
})
);
if (secret.type === SecretType.Personal && secret.userId !== actorId) {
throw new ForbiddenRequestError({
message: "You are not allowed to access this secret",
name: "GetSecretById"
});
}
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: secret.projectId
});
const secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "";
const secretComment = secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: "";
return reshapeBridgeSecret(secret.projectId, folderWithPath.environmentSlug, folderWithPath.path, {
...secret,
value: secretValue,
comment: secretComment
});
};
const getSecretByName = async ({ const getSecretByName = async ({
actorId, actorId,
actor, actor,
@ -1230,6 +1312,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx tx
}) })
); );
@ -1490,6 +1576,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
resourceMetadataDAL resourceMetadataDAL
}); });
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
@ -1522,6 +1612,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx tx
}); });
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
@ -1689,14 +1783,19 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId: folder.projectId projectId: folder.projectId
}); });
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
return secretVersions.map((el) => offset,
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { limit,
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
}) });
); });
}; };
// this is a backfilling API for secret references // this is a backfilling API for secret references
@ -1956,6 +2055,10 @@ export const secretV2BridgeServiceFactory = ({
secretTagDAL, secretTagDAL,
resourceMetadataDAL, resourceMetadataDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
inputSecrets: locallyCreatedSecrets.map((doc) => { inputSecrets: locallyCreatedSecrets.map((doc) => {
return { return {
type: doc.type, type: doc.type,
@ -1982,6 +2085,10 @@ export const secretV2BridgeServiceFactory = ({
tx, tx,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
inputSecrets: locallyUpdatedSecrets.map((doc) => { inputSecrets: locallyUpdatedSecrets.map((doc) => {
return { return {
filter: { filter: {
@ -2204,6 +2311,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsCountMultiEnv, getSecretsCountMultiEnv,
getSecretsMultiEnv, getSecretsMultiEnv,
getSecretReferenceTree, getSecretReferenceTree,
getSecretsByFolderMappings getSecretsByFolderMappings,
getSecretById
}; };
}; };

View File

@ -168,6 +168,10 @@ export type TFnSecretBulkInsert = {
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: {
type: string;
actorId: string;
};
}; };
type TRequireReferenceIfValue = type TRequireReferenceIfValue =
@ -192,6 +196,10 @@ export type TFnSecretBulkUpdate = {
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: {
type: string;
actorId: string;
};
tx?: Knex; tx?: Knex;
}; };

View File

@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@ -119,11 +120,67 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`); logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
}; };
const findVersionsBySecretIdWithActors = async (
secretId: string,
projectId: string,
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
tx?: Knex
) => {
try {
const query = (tx || db)(TableName.SecretVersionV2)
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
.leftJoin(
TableName.ProjectMembership,
`${TableName.ProjectMembership}.userId`,
`${TableName.SecretVersionV2}.userActorId`
)
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
.where((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
})
.orWhere((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
})
.select(
selectAllTableCols(TableName.SecretVersionV2),
`${TableName.Users}.username as userActorName`,
`${TableName.Identity}.name as identityActorName`,
`${TableName.ProjectMembership}.id as membershipId`
);
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(
sort.map(([column, order, nulls]) => ({
column: `${TableName.SecretVersionV2}.${column as string}`,
order,
nulls
}))
);
}
const docs: Array<
TSecretVersionsV2 & {
userActorName: string | undefined | null;
identityActorName: string | undefined | null;
membershipId: string | undefined | null;
}
> = await query;
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
}
};
return { return {
...secretVersionV2Orm, ...secretVersionV2Orm,
pruneExcessVersions, pruneExcessVersions,
findLatestVersionMany, findLatestVersionMany,
bulkUpdate, bulkUpdate,
findLatestVersionByFolderId findLatestVersionByFolderId,
findVersionsBySecretIdWithActors
}; };
}; };

View File

@ -579,6 +579,7 @@ export const fnSecretBulkInsert = async ({
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id [`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
})) }))
); );
const secretVersions = await secretVersionDAL.insertMany( const secretVersions = await secretVersionDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ sanitizedInputSecrets.map((el) => ({
...el, ...el,

View File

@ -61,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns"; import { fnTriggerWebhook } from "../webhook/webhook-fns";
import { WebhookEvents } from "../webhook/webhook-types";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { interpolateSecrets } from "./secret-fns"; import { interpolateSecrets } from "./secret-fns";
import { import {
@ -623,7 +624,14 @@ export const secretQueueFactory = ({
await queueService.queue( await queueService.queue(
QueueName.SecretWebhook, QueueName.SecretWebhook,
QueueJobs.SecWebhook, QueueJobs.SecWebhook,
{ environment, projectId, secretPath }, {
type: WebhookEvents.SecretModified,
payload: {
environment,
projectId,
secretPath
}
},
{ {
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`, jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
removeOnFail: { count: 5 }, removeOnFail: { count: 5 },
@ -1055,6 +1063,8 @@ export const secretQueueFactory = ({
const organization = await orgDAL.findOrgByProjectId(projectId); const organization = await orgDAL.findOrgByProjectId(projectId);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
const secret = await secretV2BridgeDAL.findById(data.secretId);
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
if (!organization) { if (!organization) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`); logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
@ -1083,6 +1093,19 @@ export const secretQueueFactory = ({
organizationName: organization.name organizationName: organization.name
} }
}); });
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
type: WebhookEvents.SecretReminderExpired,
payload: {
projectName: project.name,
projectId: project.id,
secretPath: folder?.path,
environment: folder?.environmentSlug || "",
reminderNote: data.note,
secretName: secret?.key,
secretId: data.secretId
}
});
}); });
const startSecretV2Migration = async (projectId: string) => { const startSecretV2Migration = async (projectId: string) => {
@ -1490,14 +1513,17 @@ export const secretQueueFactory = ({
queueService.start(QueueName.SecretWebhook, async (job) => { queueService.start(QueueName.SecretWebhook, async (job) => {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId: job.data.projectId projectId: job.data.payload.projectId
}); });
await fnTriggerWebhook({ await fnTriggerWebhook({
...job.data, projectId: job.data.payload.projectId,
environment: job.data.payload.environment,
secretPath: job.data.payload.secretPath || "/",
projectEnvDAL, projectEnvDAL,
webhookDAL,
projectDAL, projectDAL,
webhookDAL,
event: job.data,
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
}); });
}); });

View File

@ -71,6 +71,7 @@ import {
TDeleteManySecretRawDTO, TDeleteManySecretRawDTO,
TDeleteSecretDTO, TDeleteSecretDTO,
TDeleteSecretRawDTO, TDeleteSecretRawDTO,
TGetASecretByIdRawDTO,
TGetASecretDTO, TGetASecretDTO,
TGetASecretRawDTO, TGetASecretRawDTO,
TGetSecretAccessListDTO, TGetSecretAccessListDTO,
@ -95,7 +96,7 @@ type TSecretServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds"
>; >;
secretV2BridgeService: TSecretV2BridgeServiceFactory; secretV2BridgeService: TSecretV2BridgeServiceFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
@ -1382,6 +1383,18 @@ export const secretServiceFactory = ({
}; };
}; };
const getSecretByIdRaw = async ({ secretId, actorId, actor, actorOrgId, actorAuthMethod }: TGetASecretByIdRawDTO) => {
const secret = await secretV2BridgeService.getSecretById({
secretId,
actorId,
actor,
actorOrgId,
actorAuthMethod
});
return secret;
};
const getSecretByNameRaw = async ({ const getSecretByNameRaw = async ({
type, type,
path, path,
@ -3088,6 +3101,7 @@ export const secretServiceFactory = ({
getSecretsRawMultiEnv, getSecretsRawMultiEnv,
getSecretReferenceTree, getSecretReferenceTree,
getSecretsRawByFolderMappings, getSecretsRawByFolderMappings,
getSecretAccessList getSecretAccessList,
getSecretByIdRaw
}; };
}; };

View File

@ -121,6 +121,10 @@ export type TGetASecretDTO = {
version?: number; version?: number;
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretByIdDTO = {
secretId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateBulkSecretDTO = { export type TCreateBulkSecretDTO = {
path: string; path: string;
environment: string; environment: string;
@ -213,6 +217,10 @@ export type TGetASecretRawDTO = {
projectId?: string; projectId?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetASecretByIdRawDTO = {
secretId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateSecretRawDTO = TProjectPermission & { export type TCreateSecretRawDTO = TProjectPermission & {
secretName: string; secretName: string;
secretPath: string; secretPath: string;

View File

@ -50,6 +50,7 @@ const buildSlackPayload = (notification: TSlackNotification) => {
const messageBody = `A secret approval request has been opened by ${payload.userEmail}. const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
*Environment*: ${payload.environment} *Environment*: ${payload.environment}
*Secret path*: ${payload.secretPath || "/"} *Secret path*: ${payload.secretPath || "/"}
*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")}
View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${
payload.requestId payload.requestId

View File

@ -62,6 +62,7 @@ export type TSlackNotification =
secretPath: string; secretPath: string;
requestId: string; requestId: string;
projectId: string; projectId: string;
secretKeys: string[];
}; };
} }
| { | {

View File

@ -39,7 +39,8 @@ export enum SmtpTemplates {
SecretSyncFailed = "secretSyncFailed.handlebars", SecretSyncFailed = "secretSyncFailed.handlebars",
ExternalImportSuccessful = "externalImportSuccessful.handlebars", ExternalImportSuccessful = "externalImportSuccessful.handlebars",
ExternalImportFailed = "externalImportFailed.handlebars", ExternalImportFailed = "externalImportFailed.handlebars",
ExternalImportStarted = "externalImportStarted.handlebars" ExternalImportStarted = "externalImportStarted.handlebars",
SecretRequestCompleted = "secretRequestCompleted.handlebars"
} }
export enum SmtpHost { export enum SmtpHost {

View File

@ -0,0 +1,33 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Request Completed</title>
</head>
<body>
<h2>Infisical</h2>
<h2>A secret has been shared with you</h2>
{{#if name}}
<p>Secret request name: {{name}}</p>
{{/if}}
{{#if respondentUsername}}
<p>Shared by: {{respondentUsername}}</p>
{{/if}}
<br />
<br/>
<p>
You can access the secret by clicking the link below.
</p>
<p>
<a href="{{secretRequestUrl}}">Access Secret</a>
</p>
{{emailFooter}}
</body>
</html>

View File

@ -291,6 +291,15 @@ export const superAdminServiceFactory = ({
return user; return user;
}; };
const grantServerAdminAccessToUser = async (userId: string) => {
if (!licenseService.onPremFeatures?.instanceUserManagement) {
throw new BadRequestError({
message: "Failed to grant server admin access to user due to plan restriction. Upgrade to Infisical's Pro plan."
});
}
await userDAL.updateById(userId, { superAdmin: true });
};
const getAdminSlackConfig = async () => { const getAdminSlackConfig = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
@ -381,6 +390,7 @@ export const superAdminServiceFactory = ({
deleteUser, deleteUser,
getAdminSlackConfig, getAdminSlackConfig,
updateRootEncryptionStrategy, updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies getConfiguredEncryptionStrategies,
grantServerAdminAccessToUser
}; };
}; };

View File

@ -13,7 +13,9 @@ export enum PostHogEventTypes {
IntegrationCreated = "Integration Created", IntegrationCreated = "Integration Created",
MachineIdentityCreated = "Machine Identity Created", MachineIdentityCreated = "Machine Identity Created",
UserOrgInvitation = "User Org Invitation", UserOrgInvitation = "User Org Invitation",
TelemetryInstanceStats = "Self Hosted Instance Stats" TelemetryInstanceStats = "Self Hosted Instance Stats",
SecretRequestCreated = "Secret Request Created",
SecretRequestDeleted = "Secret Request Deleted"
} }
export type TSecretModifiedEvent = { export type TSecretModifiedEvent = {
@ -120,6 +122,23 @@ export type TTelemetryInstanceStatsEvent = {
}; };
}; };
export type TSecretRequestCreatedEvent = {
event: PostHogEventTypes.SecretRequestCreated;
properties: {
secretRequestId: string;
organizationId: string;
secretRequestName?: string;
};
};
export type TSecretRequestDeletedEvent = {
event: PostHogEventTypes.SecretRequestDeleted;
properties: {
secretRequestId: string;
organizationId: string;
};
};
export type TPostHogEvent = { distinctId: string } & ( export type TPostHogEvent = { distinctId: string } & (
| TSecretModifiedEvent | TSecretModifiedEvent
| TAdminInitEvent | TAdminInitEvent
@ -130,4 +149,6 @@ export type TPostHogEvent = { distinctId: string } & (
| TIntegrationCreatedEvent | TIntegrationCreatedEvent
| TProjectCreateEvent | TProjectCreateEvent
| TTelemetryInstanceStatsEvent | TTelemetryInstanceStatsEvent
| TSecretRequestCreatedEvent
| TSecretRequestDeletedEvent
); );

View File

@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal"; import { TWebhookDALFactory } from "./webhook-dal";
import { WebhookType } from "./webhook-types"; import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
@ -54,29 +54,64 @@ export const triggerWebhookRequest = async (
return req; return req;
}; };
export const getWebhookPayload = ( export const getWebhookPayload = (event: TWebhookPayloads) => {
eventName: string, if (event.type === WebhookEvents.SecretModified) {
details: { const { projectName, projectId, environment, secretPath, type } = event.payload;
workspaceName: string;
workspaceId: string; switch (type) {
environment: string; case WebhookType.SLACK:
secretPath?: string; return {
type?: string | null; text: "A secret value has been added or modified.",
attachments: [
{
color: "#E7F256",
fields: [
{
title: "Project",
value: projectName,
short: false
},
{
title: "Environment",
value: environment,
short: false
},
{
title: "Secret Path",
value: secretPath,
short: false
}
]
}
]
};
case WebhookType.GENERAL:
default:
return {
event: event.type,
project: {
workspaceId: projectId,
projectName,
environment,
secretPath
}
};
}
} }
) => {
const { workspaceName, workspaceId, environment, secretPath, type } = details; const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload;
switch (type) { switch (type) {
case WebhookType.SLACK: case WebhookType.SLACK:
return { return {
text: "A secret value has been added or modified.", text: "You have a secret reminder",
attachments: [ attachments: [
{ {
color: "#E7F256", color: "#E7F256",
fields: [ fields: [
{ {
title: "Project", title: "Project",
value: workspaceName, value: projectName,
short: false short: false
}, },
{ {
@ -88,6 +123,16 @@ export const getWebhookPayload = (
title: "Secret Path", title: "Secret Path",
value: secretPath, value: secretPath,
short: false short: false
},
{
title: "Secret Name",
value: secretName,
short: false
},
{
title: "Reminder Note",
value: reminderNote,
short: false
} }
] ]
} }
@ -96,11 +141,14 @@ export const getWebhookPayload = (
case WebhookType.GENERAL: case WebhookType.GENERAL:
default: default:
return { return {
event: eventName, event: event.type,
project: { project: {
workspaceId, workspaceId: projectId,
projectName,
environment, environment,
secretPath secretPath,
secretName,
reminderNote
} }
}; };
} }
@ -110,6 +158,7 @@ export type TFnTriggerWebhookDTO = {
projectId: string; projectId: string;
secretPath: string; secretPath: string;
environment: string; environment: string;
event: TWebhookPayloads;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">; webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
@ -124,8 +173,9 @@ export const fnTriggerWebhook = async ({
projectId, projectId,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
projectDAL, event,
secretManagerDecryptor secretManagerDecryptor,
projectDAL
}: TFnTriggerWebhookDTO) => { }: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter( const toBeTriggeredHooks = webhooks.filter(
@ -134,21 +184,20 @@ export const fnTriggerWebhook = async ({
); );
if (!toBeTriggeredHooks.length) return; if (!toBeTriggeredHooks.length) return;
logger.info({ environment, secretPath, projectId }, "Secret webhook job started"); logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
const project = await projectDAL.findById(projectId); let { projectName } = event.payload;
if (!projectName) {
const project = await projectDAL.findById(event.payload.projectId);
projectName = project.name;
}
const webhooksTriggered = await Promise.allSettled( const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) => toBeTriggeredHooks.map((hook) => {
triggerWebhookRequest( const formattedEvent = {
hook, type: event.type,
secretManagerDecryptor, payload: { ...event.payload, type: hook.type, projectName }
getWebhookPayload("secrets.modified", { } as TWebhookPayloads;
workspaceName: project.name, return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent));
workspaceId: projectId, })
environment,
secretPath,
type: hook.type
})
)
)
); );
// filter hooks by status // filter hooks by status

View File

@ -16,7 +16,8 @@ import {
TDeleteWebhookDTO, TDeleteWebhookDTO,
TListWebhookDTO, TListWebhookDTO,
TTestWebhookDTO, TTestWebhookDTO,
TUpdateWebhookDTO TUpdateWebhookDTO,
WebhookEvents
} from "./webhook-types"; } from "./webhook-types";
type TWebhookServiceFactoryDep = { type TWebhookServiceFactoryDep = {
@ -144,12 +145,15 @@ export const webhookServiceFactory = ({
await triggerWebhookRequest( await triggerWebhookRequest(
webhook, webhook,
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(), (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
getWebhookPayload("test", { getWebhookPayload({
workspaceName: project.name, type: "test" as WebhookEvents.SecretModified,
workspaceId: webhook.projectId, payload: {
environment: webhook.environment.slug, projectName: project.name,
secretPath: webhook.secretPath, projectId: webhook.projectId,
type: webhook.type environment: webhook.environment.slug,
secretPath: webhook.secretPath,
type: webhook.type
}
}) })
); );
} catch (err) { } catch (err) {

View File

@ -30,3 +30,36 @@ export enum WebhookType {
GENERAL = "general", GENERAL = "general",
SLACK = "slack" SLACK = "slack"
} }
export enum WebhookEvents {
SecretModified = "secrets.modified",
SecretReminderExpired = "secrets.reminder-expired",
TestEvent = "test"
}
type TWebhookSecretModifiedEventPayload = {
type: WebhookEvents.SecretModified;
payload: {
projectName?: string;
projectId: string;
environment: string;
secretPath?: string;
type?: string | null;
};
};
type TWebhookSecretReminderEventPayload = {
type: WebhookEvents.SecretReminderExpired;
payload: {
projectName?: string;
projectId: string;
environment: string;
secretPath?: string;
type?: string | null;
secretName: string;
secretId: string;
reminderNote?: string | null;
};
};
export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload;

View File

@ -0,0 +1,8 @@
public_ip: 127.0.0.1
auth_secret: example-auth-secret
realm: infisical.org
# set port 5349 for tls
# port: 5349
# tls_private_key_path: /full-path
# tls_ca_path: /full-path
# tls_cert_path: /full-path

View File

@ -0,0 +1,8 @@
public_ip: 127.0.0.1
auth_secret: changeThisOnProduction
realm: infisical.org
# set port 5349 for tls
# port: 5349
# tls_private_key_path: /full-path
# tls_ca_path: /full-path
# tls_cert_path: /full-path

View File

@ -1,6 +1,8 @@
module github.com/Infisical/infisical-merge module github.com/Infisical/infisical-merge
go 1.21 go 1.23.0
toolchain go1.23.5
require ( require (
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 github.com/bradleyjkemp/cupaloy/v2 v2.8.0
@ -21,12 +23,14 @@ require (
github.com/pion/logging v0.2.3 github.com/pion/logging v0.2.3
github.com/pion/turn/v4 v4.0.0 github.com/pion/turn/v4 v4.0.0
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
github.com/quic-go/quic-go v0.50.0
github.com/rs/cors v1.11.0 github.com/rs/cors v1.11.0
github.com/rs/zerolog v1.26.1 github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.8.1 github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.33.0 golang.org/x/crypto v0.35.0
golang.org/x/sys v0.30.0
golang.org/x/term v0.29.0 golang.org/x/term v0.29.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@ -58,13 +62,15 @@ require (
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/errors v0.20.2 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
github.com/google/s2a-go v0.1.7 // indirect github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect github.com/googleapis/gax-go/v2 v2.12.5 // indirect
@ -82,6 +88,7 @@ require (
github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.15.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
@ -103,17 +110,20 @@ require (
go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.33.0 // indirect go.uber.org/mock v0.5.0 // indirect
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.6.0 // indirect golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.30.0 // indirect
google.golang.org/api v0.188.0 // indirect google.golang.org/api v0.188.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
google.golang.org/grpc v1.64.1 // indirect google.golang.org/grpc v1.64.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
@ -129,3 +139,5 @@ require (
) )
replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.2 replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.2
replace github.com/pion/turn/v4 => github.com/Infisical/turn/v4 v4.0.1

View File

@ -49,6 +49,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8= github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8=
github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto= github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto=
github.com/Infisical/turn/v4 v4.0.1 h1:omdelNsnFfzS5cu86W5OBR68by68a8sva4ogR0lQQnw=
github.com/Infisical/turn/v4 v4.0.1/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -144,8 +146,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8=
@ -154,6 +156,8 @@ github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtK
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -222,6 +226,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
@ -342,6 +348,10 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@ -357,8 +367,6 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -369,6 +377,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a h1:Ey0XWvrg6u6hyIn1Kd/jCCmL+bMv9El81tvuGBbxZGg= github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a h1:Ey0XWvrg6u6hyIn1Kd/jCCmL+bMv9El81tvuGBbxZGg=
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo=
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -415,8 +425,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
@ -461,6 +471,8 @@ go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8p
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -472,8 +484,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -484,6 +496,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c=
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -509,6 +523,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -547,8 +563,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -697,6 +713,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -811,8 +829,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -109,12 +109,38 @@ var gatewayCmd = &cobra.Command{
}, },
} }
var gatewayRelayCmd = &cobra.Command{
Example: `infisical gateway relay`,
Short: "Used to run infisical gateway relay",
Use: "relay",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
relayConfigFilePath, err := cmd.Flags().GetString("config")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if relayConfigFilePath == "" {
util.HandleError(fmt.Errorf("Missing config file"))
}
gatewayRelay, err := gateway.NewGatewayRelay(relayConfigFilePath)
if err != nil {
util.HandleError(err, "Failed to initialize gateway")
}
err = gatewayRelay.Run()
if err != nil {
util.HandleError(err, "Failed to start gateway")
}
},
}
func init() { func init() {
gatewayCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
command.Flags().MarkHidden("domain")
command.Parent().HelpFunc()(command, strings)
})
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token") gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
gatewayCmd.AddCommand(gatewayRelayCmd)
rootCmd.AddCommand(gatewayCmd) rootCmd.AddCommand(gatewayCmd)
} }

View File

@ -3,20 +3,49 @@ package gateway
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"errors" "errors"
"io" "io"
"net" "net"
"strings"
"sync" "sync"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func handleConnection(conn net.Conn) { func handleConnection(ctx context.Context, quicConn quic.Connection) {
defer conn.Close() log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String())
log.Info().Msgf("New connection from: %s", conn.RemoteAddr().String()) // Use WaitGroup to track all streams
var wg sync.WaitGroup
for {
// Accept the first stream, which we'll use for commands
stream, err := quicConn.AcceptStream(ctx)
if err != nil {
log.Printf("Failed to accept QUIC stream: %v", err)
break
}
wg.Add(1)
go func(stream quic.Stream) {
defer wg.Done()
defer stream.Close()
handleStream(stream, quicConn)
}(stream)
}
wg.Wait()
log.Printf("All streams closed for connection: %s", quicConn.RemoteAddr().String())
}
func handleStream(stream quic.Stream, quicConn quic.Connection) {
streamID := stream.StreamID()
log.Printf("New stream %d from: %s", streamID, quicConn.RemoteAddr().String())
// Use buffered reader for better handling of fragmented data // Use buffered reader for better handling of fragmented data
reader := bufio.NewReader(conn) reader := bufio.NewReader(stream)
defer stream.Close()
for { for {
msg, err := reader.ReadBytes('\n') msg, err := reader.ReadBytes('\n')
if err != nil { if err != nil {
@ -39,6 +68,7 @@ func handleConnection(conn net.Conn) {
return return
} }
defer destTarget.Close() defer destTarget.Close()
log.Info().Msgf("Starting secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String())
// Handle buffered data // Handle buffered data
buffered := reader.Buffered() buffered := reader.Buffered()
@ -56,10 +86,11 @@ func handleConnection(conn net.Conn) {
} }
} }
CopyData(conn, destTarget) CopyDataFromQuicToTcp(stream, destTarget)
log.Info().Msgf("Ending secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String())
return return
case "PING": case "PING":
if _, err := conn.Write([]byte("PONG")); err != nil { if _, err := stream.Write([]byte("PONG\n")); err != nil {
log.Error().Msgf("Error writing PONG response: %v", err) log.Error().Msgf("Error writing PONG response: %v", err)
} }
return return
@ -74,34 +105,38 @@ type CloseWrite interface {
CloseWrite() error CloseWrite() error
} }
func CopyData(src, dst net.Conn) { func CopyDataFromQuicToTcp(quicStream quic.Stream, tcpConn net.Conn) {
// Create a WaitGroup to wait for both copy operations
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)
copyAndClose := func(dst, src net.Conn, done chan<- bool) { // Start copying from QUIC stream to TCP
go func() {
defer wg.Done() defer wg.Done()
_, err := io.Copy(dst, src) if _, err := io.Copy(tcpConn, quicStream); err != nil {
if err != nil && !errors.Is(err, io.EOF) { log.Error().Msgf("Error copying quic->postgres: %v", err)
log.Error().Msgf("Copy error: %v", err)
} }
// Signal we're done writing if e, ok := tcpConn.(CloseWrite); ok {
done <- true log.Debug().Msg("Closing TCP write end")
e.CloseWrite()
// Half close the connection if possible } else {
if c, ok := dst.(CloseWrite); ok { log.Debug().Msg("TCP connection does not support CloseWrite")
c.CloseWrite()
} }
} }()
done1 := make(chan bool, 1) // Start copying from TCP to QUIC stream
done2 := make(chan bool, 1) go func() {
defer wg.Done()
go copyAndClose(dst, src, done1) if _, err := io.Copy(quicStream, tcpConn); err != nil {
go copyAndClose(src, dst, done2) log.Debug().Msgf("Error copying postgres->quic: %v", err)
}
// Close the write side of the QUIC stream
if err := quicStream.Close(); err != nil && !strings.Contains(err.Error(), "close called for canceled stream") {
log.Error().Msgf("Error closing QUIC stream write: %v", err)
}
}()
// Wait for both copies to complete // Wait for both copies to complete
<-done1
<-done2
wg.Wait() wg.Wait()
} }

View File

@ -6,15 +6,19 @@ import (
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"net" "net"
"os"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/systemd"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/pion/logging" "github.com/pion/logging"
"github.com/pion/turn/v4" "github.com/pion/turn/v4"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/quic-go/quic-go"
) )
type GatewayConfig struct { type GatewayConfig struct {
@ -56,13 +60,12 @@ func (g *Gateway) ConnectWithRelay() error {
if relayPort == "5349" { if relayPort == "5349" {
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort) log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
conn, err = tls.Dial("tcp", relayDetails.TurnServerAddress, &tls.Config{ conn, err = tls.Dial("tcp", relayDetails.TurnServerAddress, &tls.Config{
InsecureSkipVerify: false, ServerName: relayAddress,
ServerName: relayAddress,
}) })
} else { } else {
log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort) log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort)
peerAddr, err := net.ResolveTCPAddr("tcp", relayDetails.TurnServerAddress) peerAddr, errPeer := net.ResolveTCPAddr("tcp", relayDetails.TurnServerAddress)
if err != nil { if errPeer != nil {
return fmt.Errorf("Failed to parse turn server address: %w", err) return fmt.Errorf("Failed to parse turn server address: %w", err)
} }
conn, err = net.DialTCP("tcp", nil, peerAddr) conn, err = net.DialTCP("tcp", nil, peerAddr)
@ -74,6 +77,10 @@ func (g *Gateway) ConnectWithRelay() error {
// Start a new TURN Client and wrap our net.Conn in a STUNConn // Start a new TURN Client and wrap our net.Conn in a STUNConn
// This allows us to simulate datagram based communication over a net.Conn // This allows us to simulate datagram based communication over a net.Conn
logger := logging.NewDefaultLoggerFactory()
if os.Getenv("LOG_LEVEL") == "debug" {
logger.DefaultLogLevel = logging.LogLevelDebug
}
cfg := &turn.ClientConfig{ cfg := &turn.ClientConfig{
STUNServerAddr: relayDetails.TurnServerAddress, STUNServerAddr: relayDetails.TurnServerAddress,
TURNServerAddr: relayDetails.TurnServerAddress, TURNServerAddr: relayDetails.TurnServerAddress,
@ -81,7 +88,7 @@ func (g *Gateway) ConnectWithRelay() error {
Username: relayDetails.TurnServerUsername, Username: relayDetails.TurnServerUsername,
Password: relayDetails.TurnServerPassword, Password: relayDetails.TurnServerPassword,
Realm: relayDetails.TurnServerRealm, Realm: relayDetails.TurnServerRealm,
LoggerFactory: logging.NewDefaultLoggerFactory(), LoggerFactory: logger,
} }
client, err := turn.NewClient(cfg) client, err := turn.NewClient(cfg)
@ -95,10 +102,6 @@ func (g *Gateway) ConnectWithRelay() error {
TurnServerAddress: relayDetails.TurnServerAddress, TurnServerAddress: relayDetails.TurnServerAddress,
InfisicalStaticIp: relayDetails.InfisicalStaticIp, InfisicalStaticIp: relayDetails.InfisicalStaticIp,
} }
// if port not specific allow all port
if relayDetails.InfisicalStaticIp != "" && !strings.Contains(relayDetails.InfisicalStaticIp, ":") {
g.config.InfisicalStaticIp = g.config.InfisicalStaticIp + ":0"
}
g.client = client g.client = client
return nil return nil
@ -116,20 +119,20 @@ func (g *Gateway) Listen(ctx context.Context) error {
// Allocate a relay socket on the TURN server. On success, it // Allocate a relay socket on the TURN server. On success, it
// will return a net.PacketConn which represents the remote // will return a net.PacketConn which represents the remote
// socket. // socket.
relayNonTlsConn, err := g.client.AllocateTCP() relayUdpConnection, err := g.client.Allocate()
if err != nil { if err != nil {
return fmt.Errorf("Failed to allocate relay connection: %w", err) return fmt.Errorf("Failed to allocate relay connection: %w", err)
} }
log.Info().Msg(relayNonTlsConn.Addr().String()) log.Info().Msg(relayUdpConnection.LocalAddr().String())
defer func() { defer func() {
if closeErr := relayNonTlsConn.Close(); closeErr != nil { if closeErr := relayUdpConnection.Close(); closeErr != nil {
log.Error().Msgf("Failed to close connection: %s", closeErr) log.Error().Msgf("Failed to close connection: %s", closeErr)
} }
}() }()
gatewayCert, err := api.CallExchangeRelayCertV1(g.httpClient, api.ExchangeRelayCertRequestV1{ gatewayCert, err := api.CallExchangeRelayCertV1(g.httpClient, api.ExchangeRelayCertRequestV1{
RelayAddress: relayNonTlsConn.Addr().String(), RelayAddress: relayUdpConnection.LocalAddr().String(),
}) })
if err != nil { if err != nil {
return err return err
@ -140,49 +143,54 @@ func (g *Gateway) Listen(ctx context.Context) error {
g.config.Certificate = gatewayCert.Certificate g.config.Certificate = gatewayCert.Certificate
g.config.CertificateChain = gatewayCert.CertificateChain g.config.CertificateChain = gatewayCert.CertificateChain
errCh := make(chan error, 1)
shutdownCh := make(chan bool, 1) shutdownCh := make(chan bool, 1)
if g.config.InfisicalStaticIp != "" { if err = g.createPermissionForStaticIps(g.config.InfisicalStaticIp); err != nil {
log.Info().Msgf("Found static ip from Infisical: %s. Creating permission IP lifecycle", g.config.InfisicalStaticIp) return err
peerAddr, err := net.ResolveTCPAddr("tcp", g.config.InfisicalStaticIp)
if err != nil {
return fmt.Errorf("Failed to parse infisical static ip: %w", err)
}
g.registerPermissionLifecycle(func() error {
err := relayNonTlsConn.CreatePermissions(peerAddr)
return err
}, shutdownCh)
} }
g.registerHeartBeat(ctx, errCh)
cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey)) cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey))
if err != nil { if err != nil {
return fmt.Errorf("failed to parse cert: %s", err) return fmt.Errorf("failed to parse cert: %w", err)
} }
caCertPool := x509.NewCertPool() caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(gatewayCert.CertificateChain)) caCertPool.AppendCertsFromPEM([]byte(gatewayCert.CertificateChain))
relayConn := tls.NewListener(relayNonTlsConn, &tls.Config{ // Setup QUIC server
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert}, Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
ClientCAs: caCertPool, ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert, ClientAuth: tls.RequireAndVerifyClientCert,
}) NextProtos: []string{"infisical-gateway"},
}
// Setup QUIC listener on the relayConn
quicConfig := &quic.Config{
EnableDatagrams: true,
MaxIdleTimeout: 10 * time.Second,
KeepAlivePeriod: 2 * time.Second,
}
g.registerRelayIsActive(ctx, errCh)
quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig)
if err != nil {
return fmt.Errorf("Failed to listen for QUIC: %w", err)
}
defer quicListener.Close()
log.Printf("Listener started on %s", quicListener.Addr())
errCh := make(chan error, 1)
log.Info().Msg("Gateway started successfully") log.Info().Msg("Gateway started successfully")
g.registerHeartBeat(errCh, shutdownCh)
g.registerRelayIsActive(relayNonTlsConn.Addr().String(), errCh, shutdownCh)
// Create a WaitGroup to track active connections
var wg sync.WaitGroup var wg sync.WaitGroup
go func() { go func() {
for { for {
if relayDeadlineConn, ok := relayConn.(*net.TCPListener); ok {
relayDeadlineConn.SetDeadline(time.Now().Add(1 * time.Second))
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
@ -190,75 +198,53 @@ func (g *Gateway) Listen(ctx context.Context) error {
return return
default: default:
// Accept new relay connection // Accept new relay connection
conn, err := relayConn.Accept() quicConn, err := quicListener.Accept(context.Background())
if err != nil { if err != nil {
// Check if it's a timeout error (which we expect due to our deadline) log.Printf("Failed to accept QUIC connection: %v", err)
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
if !strings.Contains(err.Error(), "data contains incomplete STUN or TURN frame") {
log.Error().Msgf("Failed to accept connection: %v", err)
}
continue continue
} }
tlsConn, ok := conn.(*tls.Conn) tlsState := quicConn.ConnectionState().TLS
if !ok { if len(tlsState.PeerCertificates) > 0 {
log.Error().Msg("Failed to convert to TLS connection") organizationUnit := tlsState.PeerCertificates[0].Subject.OrganizationalUnit
conn.Close() commonName := tlsState.PeerCertificates[0].Subject.CommonName
continue
}
// Set a deadline for the handshake to prevent hanging
tlsConn.SetDeadline(time.Now().Add(10 * time.Second))
err = tlsConn.Handshake()
// Clear the deadline after handshake
tlsConn.SetDeadline(time.Time{})
if err != nil {
log.Error().Msgf("TLS handshake failed: %v", err)
conn.Close()
continue
}
// Get connection state which contains certificate information
state := tlsConn.ConnectionState()
if len(state.PeerCertificates) > 0 {
organizationUnit := state.PeerCertificates[0].Subject.OrganizationalUnit
commonName := state.PeerCertificates[0].Subject.CommonName
if organizationUnit[0] != "gateway-client" || commonName != "cloud" { if organizationUnit[0] != "gateway-client" || commonName != "cloud" {
log.Error().Msgf("Client certificate verification failed. Received %s, %s", organizationUnit, commonName) errMsg := fmt.Sprintf("Client certificate verification failed. Received %s, %s", organizationUnit, commonName)
conn.Close() log.Error().Msg(errMsg)
quicConn.CloseWithError(1, errMsg)
continue continue
} }
} }
// Handle the connection in a goroutine // Handle the connection in a goroutine
wg.Add(1) wg.Add(1)
go func(c net.Conn) { go func(c quic.Connection) {
defer wg.Done() defer wg.Done()
defer c.Close() defer c.CloseWithError(0, "connection closed")
// Monitor parent context to close this connection when needed // Monitor parent context to close this connection when needed
go func() { go func() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
c.Close() // Force close connection when context is canceled c.CloseWithError(0, "connection closed") // Force close connection when context is canceled
case <-shutdownCh: case <-shutdownCh:
c.Close() // Force close connection when accepting loop is done c.CloseWithError(0, "connection closed") // Force close connection when accepting loop is done
} }
}() }()
handleConnection(c) handleConnection(ctx, c)
}(conn) }(quicConn)
} }
} }
}() }()
// make this compatiable with systemd notify mode
systemd.SdNotify(false, systemd.SdNotifyReady)
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Info().Msg("Shutting down gateway...") log.Info().Msg("Shutting down gateway...")
case err = <-errCh: case err = <-errCh:
log.Error().Err(err).Msg("Gateway error occurred")
} }
// Signal the accept loop to stop // Signal the accept loop to stop
@ -281,70 +267,107 @@ func (g *Gateway) Listen(ctx context.Context) error {
return err return err
} }
func (g *Gateway) registerHeartBeat(errCh chan error, done chan bool) { func (g *Gateway) registerHeartBeat(ctx context.Context, errCh chan error) {
ticker := time.NewTicker(1 * time.Hour) ticker := time.NewTicker(30 * time.Minute)
defer ticker.Stop()
go func() { go func() {
time.Sleep(10 * time.Second)
log.Info().Msg("Registering first heart beat")
err := api.CallGatewayHeartBeatV1(g.httpClient)
if err != nil {
log.Error().Msgf("Failed to register heartbeat: %s", err)
}
for { for {
select { if err := api.CallGatewayHeartBeatV1(g.httpClient); err != nil {
case <-done:
ticker.Stop()
return
case <-ticker.C:
log.Info().Msg("Registering heart beat")
err := api.CallGatewayHeartBeatV1(g.httpClient)
errCh <- err errCh <- err
} else {
log.Info().Msg("Gateway is reachable by Infisical")
} }
}
}()
}
func (g *Gateway) registerPermissionLifecycle(permissionFn func() error, done chan bool) {
ticker := time.NewTicker(3 * time.Minute)
go func() {
// wait for 5 mins
permissionFn()
log.Printf("Created permission for incoming connections")
for {
select { select {
case <-done: case <-ctx.Done():
ticker.Stop()
return return
case <-ticker.C: case <-ticker.C:
permissionFn()
} }
} }
}() }()
} }
func (g *Gateway) registerRelayIsActive(serverAddr string, errCh chan error, done chan bool) { func (g *Gateway) createPermissionForStaticIps(staticIps string) error {
ticker := time.NewTicker(10 * time.Second) if staticIps == "" {
return fmt.Errorf("Missing Infisical static ips for permission")
}
splittedIps := strings.Split(staticIps, ",")
resolvedIps := make([]net.Addr, 0)
for _, ip := range splittedIps {
ip = strings.TrimSpace(ip)
if ip == "" {
continue
}
// if port not specific allow all port
if !strings.Contains(ip, ":") {
ip = ip + ":0"
}
peerAddr, err := net.ResolveUDPAddr("udp", ip)
if err != nil {
return fmt.Errorf("Failed to resolve static ip for permission: %w", err)
}
resolvedIps = append(resolvedIps, peerAddr)
}
if err := g.client.CreatePermission(resolvedIps...); err != nil {
return fmt.Errorf("Failed to set ip permission: %w", err)
}
return nil
}
func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) error {
ticker := time.NewTicker(15 * time.Second)
maxFailures := 3
failures := 0
log.Info().Msg("Starting relay connection health check")
go func() { go func() {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
for { for {
select { select {
case <-done: case <-ctx.Done():
ticker.Stop() log.Info().Msg("Stopping relay connection health check")
return return
case <-ticker.C: case <-ticker.C:
conn, err := net.Dial("tcp", serverAddr) func() {
if err != nil { log.Debug().Msg("Performing relay connection health check")
errCh <- err
return if g.client == nil {
} failures++
if conn != nil { log.Warn().Int("failures", failures).Msg("TURN client is nil")
conn.Close() if failures >= maxFailures {
} errCh <- fmt.Errorf("relay connection check failed: TURN client is nil")
}
return
}
// we try to refresh permissions - this is a lightweight operation
// that will fail immediately if the UDP connection is broken. good for health check
log.Debug().Msg("Refreshing TURN permissions to verify connection")
if err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp); err != nil {
failures++
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
if failures >= maxFailures {
errCh <- fmt.Errorf("relay connection check failed: %w", err)
}
return
}
log.Debug().Msg("Successfully refreshed TURN permissions - connection is healthy")
if failures > 0 {
log.Info().Int("previous_failures", failures).Msg("Relay connection restored")
failures = 0
}
}()
} }
} }
}() }()
return nil
} }

View File

@ -0,0 +1,190 @@
//go:build !windows
// +build !windows
package gateway
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"os"
"os/signal"
"runtime"
"strconv"
"syscall"
udplistener "github.com/Infisical/infisical-merge/packages/gateway/udp_listener"
"github.com/Infisical/infisical-merge/packages/systemd"
"github.com/pion/logging"
"github.com/pion/turn/v4"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
)
var (
errMissingTlsCert = errors.New("Missing TLS files")
)
type GatewayRelay struct {
Config *GatewayRelayConfig
}
type GatewayRelayConfig struct {
PublicIP string `yaml:"public_ip"`
Port int `yaml:"port"`
Realm string `yaml:"realm"`
AuthSecret string `yaml:"auth_secret"`
RelayMinPort uint16 `yaml:"relay_min_port"`
RelayMaxPort uint16 `yaml:"relay_max_port"`
TlsCertPath string `yaml:"tls_cert_path"`
TlsPrivateKeyPath string `yaml:"tls_private_key_path"`
TlsCaPath string `yaml:"tls_ca_path"`
tls tls.Certificate
tlsCa string
isTlsEnabled bool
}
func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) {
cfgFile, err := os.ReadFile(configFilePath)
if err != nil {
return nil, err
}
var cfg GatewayRelayConfig
if err := yaml.Unmarshal(cfgFile, &cfg); err != nil {
return nil, err
}
if cfg.PublicIP == "" {
return nil, fmt.Errorf("Missing public ip")
}
if cfg.AuthSecret == "" {
return nil, fmt.Errorf("Missing auth secret")
}
if cfg.Realm == "" {
cfg.Realm = "infisical.org"
}
if cfg.RelayMinPort == 0 {
cfg.RelayMinPort = 49152
}
if cfg.RelayMaxPort == 0 {
cfg.RelayMaxPort = 65535
}
if cfg.Port == 0 {
cfg.Port = 3478
} else if cfg.Port == 5349 {
if cfg.TlsCertPath == "" || cfg.TlsPrivateKeyPath == "" {
return nil, errMissingTlsCert
}
cert, err := tls.LoadX509KeyPair(cfg.TlsCertPath, cfg.TlsPrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("Failed to read load server tls key pair: %w", err)
}
if cfg.TlsCaPath != "" {
ca, err := os.ReadFile(cfg.TlsCaPath)
if err != nil {
return nil, fmt.Errorf("Failed to read tls ca: %w", err)
}
cfg.tlsCa = string(ca)
}
cfg.tls = cert
cfg.isTlsEnabled = true
}
return &GatewayRelay{
Config: &cfg,
}, nil
}
func (g *GatewayRelay) Run() error {
addr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:"+strconv.Itoa(g.Config.Port))
if err != nil {
return fmt.Errorf("Failed to parse server address: %s", err)
}
// NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages
// and process them yourself.
logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout)
// Create `numThreads` UDP listeners to pass into pion/turn
// pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in
// this allows us to add logging, storage or modify inbound/outbound traffic
// UDP listeners share the same local address:port with setting SO_REUSEPORT and the kernel
// will load-balance received packets per the IP 5-tuple
listenerConfig := udplistener.SetupListenerConfig()
publicIP := g.Config.PublicIP
relayAddressGenerator := &turn.RelayAddressGeneratorPortRange{
RelayAddress: net.ParseIP(publicIP), // Claim that we are listening on IP passed by user
Address: "0.0.0.0", // But actually be listening on every interface
MinPort: g.Config.RelayMinPort,
MaxPort: g.Config.RelayMaxPort,
}
threadNum := runtime.NumCPU()
listenerConfigs := make([]turn.ListenerConfig, threadNum)
var connAddress string
for i := 0; i < threadNum; i++ {
conn, listErr := listenerConfig.Listen(context.Background(), addr.Network(), addr.String())
if listErr != nil {
return fmt.Errorf("Failed to allocate TCP listener at %s:%s %s", addr.Network(), addr.String(), listErr)
}
listenerConfigs[i] = turn.ListenerConfig{
RelayAddressGenerator: relayAddressGenerator,
}
if g.Config.isTlsEnabled {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(g.Config.tlsCa))
listenerConfigs[i].Listener = tls.NewListener(conn, &tls.Config{
Certificates: []tls.Certificate{g.Config.tls},
ClientCAs: caCertPool,
})
} else {
listenerConfigs[i].Listener = conn
}
connAddress = conn.Addr().String()
}
loggerF := logging.NewDefaultLoggerFactory()
loggerF.DefaultLogLevel = logging.LogLevelDebug
server, err := turn.NewServer(turn.ServerConfig{
Realm: g.Config.Realm,
AuthHandler: turn.LongTermTURNRESTAuthHandler(g.Config.AuthSecret, logger),
// PacketConnConfigs is a list of UDP Listeners and the configuration around them
ListenerConfigs: listenerConfigs,
LoggerFactory: loggerF,
})
if err != nil {
return fmt.Errorf("Failed to start server: %w", err)
}
log.Info().Msgf("Relay listening on %s\n", connAddress)
// make this compatiable with systemd notify mode
systemd.SdNotify(false, systemd.SdNotifyReady)
// Block until user sends SIGINT or SIGTERM
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
if err = server.Close(); err != nil {
return fmt.Errorf("Failed to close server: %w", err)
}
return nil
}

View File

@ -0,0 +1,37 @@
//go:build windows
// +build windows
package gateway
import (
"errors"
)
var (
errMissingTlsCert = errors.New("Missing TLS files")
errWindowsNotSupported = errors.New("Relay is not supported on Windows")
)
type GatewayRelay struct {
Config *GatewayRelayConfig
}
type GatewayRelayConfig struct {
PublicIP string
Port int
Realm string
AuthSecret string
RelayMinPort uint16
RelayMaxPort uint16
TlsCertPath string
TlsPrivateKeyPath string
TlsCaPath string
}
func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) {
return nil, errWindowsNotSupported
}
func (g *GatewayRelay) Run() error {
return errWindowsNotSupported
}

View File

@ -0,0 +1,26 @@
//go:build !windows
// +build !windows
package udplistener
import (
"net"
"syscall"
"golang.org/x/sys/unix"
// other imports
)
func SetupListenerConfig() *net.ListenConfig {
return &net.ListenConfig{
Control: func(network, address string, conn syscall.RawConn) error {
var operr error
if err := conn.Control(func(fd uintptr) {
operr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
}); err != nil {
return err
}
return operr
},
}
}

View File

@ -0,0 +1,18 @@
//go:build windows
// +build windows
package udplistener
import (
"fmt"
"net"
"syscall"
)
func SetupListenerConfig() *net.ListenConfig {
return &net.ListenConfig{
Control: func(network, address string, conn syscall.RawConn) error {
return fmt.Errorf("Infisical relay not supported for windows.")
},
}
}

View File

@ -0,0 +1,84 @@
// Copyright 2014 Docker, Inc.
// Copyright 2015-2018 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Package daemon provides a Go implementation of the sd_notify protocol.
// It can be used to inform systemd of service start-up completion, watchdog
// events, and other status changes.
//
// https://www.freedesktop.org/software/systemd/man/sd_notify.html#Description
package systemd
import (
"net"
"os"
)
const (
// SdNotifyReady tells the service manager that service startup is finished
// or the service finished loading its configuration.
SdNotifyReady = "READY=1"
// SdNotifyStopping tells the service manager that the service is beginning
// its shutdown.
SdNotifyStopping = "STOPPING=1"
// SdNotifyReloading tells the service manager that this service is
// reloading its configuration. Note that you must call SdNotifyReady when
// it completed reloading.
SdNotifyReloading = "RELOADING=1"
// SdNotifyWatchdog tells the service manager to update the watchdog
// timestamp for the service.
SdNotifyWatchdog = "WATCHDOG=1"
)
// SdNotify sends a message to the init daemon. It is common to ignore the error.
// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET`
// will be unconditionally unset.
//
// It returns one of the following:
// (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset)
// (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data)
// (true, nil) - notification supported, data has been sent
func SdNotify(unsetEnvironment bool, state string) (bool, error) {
socketAddr := &net.UnixAddr{
Name: os.Getenv("NOTIFY_SOCKET"),
Net: "unixgram",
}
// NOTIFY_SOCKET not set
if socketAddr.Name == "" {
return false, nil
}
if unsetEnvironment {
if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil {
return false, err
}
}
conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
// Error connecting to NOTIFY_SOCKET
if err != nil {
return false, err
}
defer conn.Close()
if _, err = conn.Write([]byte(state)); err != nil {
return false, err
}
return true, nil
}

View File

@ -76,8 +76,7 @@ This tab allows you to set various rate limits for your Infisical instance. You
## User Management Tab ## User Management Tab
From this tab, you can view all the users who have signed up for your instance. You can search for users using the search bar and remove them from your instance by pressing the **X** button on their respective row. From this tab, you can view all the users who have signed up for your instance. You can search for users using the search bar and remove them from your instance by clicking on the three dots icon on the right. Additionally, the Server Admin can grant server administrator access to other users through this menu.
![User Management](/images/platform/admin-panels/admin-panel-users.png) ![User Management](/images/platform/admin-panels/admin-panel-users.png)
<Note> <Note>

View File

@ -36,3 +36,18 @@ If the signature in the header matches the signature that you generated, then yo
"timestamp": "" "timestamp": ""
} }
``` ```
```json
{
"event": "secrets.reminder-expired",
"project": {
"workspaceId": "the workspace id",
"environment": "project environment",
"secretPath": "project folder path",
"secretName": "name of the secret",
"secretId": "id of the secret",
"reminderNote": "reminder note of the secret"
},
"timestamp": ""
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 288 KiB

View File

@ -75,7 +75,7 @@ via the UI or API for the third-party service you intend to sync secrets to.
2. <strong>Create Secret Sync:</strong> Configure a Secret Sync in the desired project by specifying the following parameters via the UI or API: 2. <strong>Create Secret Sync:</strong> Configure a Secret Sync in the desired project by specifying the following parameters via the UI or API:
- <strong>Source:</strong> The project environment and folder path you wish to retrieve secrets from. - <strong>Source:</strong> The project environment and folder path you wish to retrieve secrets from.
- <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services. - <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services.
- <strong>Options:</strong> Customize how secrets should be synced. Examples include adding a suffix or prefix to your secrets, or importing secrets from the destination on the initial sync. - <strong>Options:</strong> Customize how secrets should be synced, such as whether or not secrets should be imported from the destination on the initial sync.
<Note> <Note>
Secret Syncs are the source of truth for connected third-party services. Any secret, Secret Syncs are the source of truth for connected third-party services. Any secret,

View File

@ -25,6 +25,7 @@ export const publicPaths = [
"/login/sso", "/login/sso",
"/admin/signup", "/admin/signup",
"/shared/secret/[id]", "/shared/secret/[id]",
"/secret-request/secret/[id]",
"/share-secret" "/share-secret"
]; ];

View File

@ -21,6 +21,10 @@ export const ROUTE_PATHS = Object.freeze({
"/organization/secret-scanning", "/organization/secret-scanning",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning" "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
), ),
SecretSharing: setRoute(
"/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
),
SettingsPage: setRoute( SettingsPage: setRoute(
"/organization/settings", "/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/settings" "/_authenticate/_inject-org-details/_org-layout/organization/settings"
@ -285,6 +289,10 @@ export const ROUTE_PATHS = Object.freeze({
) )
}, },
Public: { Public: {
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId") ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
ViewSecretRequestByIDPage: setRoute(
"/secret-request/secret/$secretRequestId",
"/secret-request/secret/$secretRequestId"
)
} }
}); });

View File

@ -3,7 +3,8 @@ export {
useCreateAdminUser, useCreateAdminUser,
useUpdateAdminSlackConfig, useUpdateAdminSlackConfig,
useUpdateServerConfig, useUpdateServerConfig,
useUpdateServerEncryptionStrategy useUpdateServerEncryptionStrategy,
useAdminGrantServerAdminAccess
} from "./mutation"; } from "./mutation";
export { export {
useAdminGetUsers, useAdminGetUsers,

View File

@ -70,6 +70,21 @@ export const useAdminDeleteUser = () => {
}); });
}; };
export const useAdminGrantServerAdminAccess = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userId: string) => {
await apiRequest.patch(`/api/v1/admin/user-management/users/${userId}/admin-access`);
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getUsers]
});
}
});
};
export const useUpdateAdminSlackConfig = () => { export const useUpdateAdminSlackConfig = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<AdminSlackConfig, object, TUpdateAdminSlackConfigDTO>({ return useMutation<AdminSlackConfig, object, TUpdateAdminSlackConfigDTO>({

View File

@ -122,6 +122,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
"OIDC group membership mapping assigned user to groups", "OIDC group membership mapping assigned user to groups",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]: [EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
"OIDC group membership mapping removed user from groups", "OIDC group membership mapping removed user from groups",
[EventType.SECRET_APPROVAL_REQUEST_REVIEW]: "Review Secret Approval Request",
[EventType.CREATE_KMIP_CLIENT]: "Create KMIP client", [EventType.CREATE_KMIP_CLIENT]: "Create KMIP client",
[EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client", [EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client",
[EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client", [EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client",

View File

@ -150,5 +150,6 @@ export enum EventType {
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate", KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
KMIP_OPERATION_REVOKE = "kmip-operation-revoke", KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
KMIP_OPERATION_LOCATE = "kmip-operation-locate", KMIP_OPERATION_LOCATE = "kmip-operation-locate",
KMIP_OPERATION_REGISTER = "kmip-operation-register" KMIP_OPERATION_REGISTER = "kmip-operation-register",
SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review"
} }

View File

@ -13,9 +13,10 @@ export const useUpdateSecretApprovalReviewStatus = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<object, object, TUpdateSecretApprovalReviewStatusDTO>({ return useMutation<object, object, TUpdateSecretApprovalReviewStatusDTO>({
mutationFn: async ({ id, status }) => { mutationFn: async ({ id, status, comment }) => {
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, { const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, {
status status,
comment
}); });
return data; return data;
}, },

View File

@ -44,6 +44,7 @@ export type TSecretApprovalRequest = {
reviewers: { reviewers: {
userId: string; userId: string;
status: ApprovalStatus; status: ApprovalStatus;
comment: string;
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
@ -114,6 +115,7 @@ export type TGetSecretApprovalRequestDetails = {
export type TUpdateSecretApprovalReviewStatusDTO = { export type TUpdateSecretApprovalReviewStatusDTO = {
status: ApprovalStatus; status: ApprovalStatus;
comment?: string;
id: string; id: string;
}; };

View File

@ -148,12 +148,13 @@ export const useUpdateFolder = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<object, object, TUpdateFolderDTO>({ return useMutation<object, object, TUpdateFolderDTO>({
mutationFn: async ({ path = "/", folderId, name, environment, projectId }) => { mutationFn: async ({ path = "/", folderId, name, environment, projectId, description }) => {
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, { const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
name, name,
environment, environment,
workspaceId: projectId, workspaceId: projectId,
path path,
description
}); });
return data; return data;
}, },

View File

@ -5,6 +5,7 @@ export enum ReservedFolders {
export type TSecretFolder = { export type TSecretFolder = {
id: string; id: string;
name: string; name: string;
description?: string;
}; };
export type TGetProjectFoldersDTO = { export type TGetProjectFoldersDTO = {
@ -24,6 +25,7 @@ export type TCreateFolderDTO = {
environment: string; environment: string;
name: string; name: string;
path?: string; path?: string;
description?: string | null;
}; };
export type TUpdateFolderDTO = { export type TUpdateFolderDTO = {
@ -32,6 +34,7 @@ export type TUpdateFolderDTO = {
name: string; name: string;
folderId: string; folderId: string;
path?: string; path?: string;
description?: string | null;
}; };
export type TDeleteFolderDTO = { export type TDeleteFolderDTO = {
@ -49,5 +52,6 @@ export type TUpdateFolderBatchDTO = {
environment: string; environment: string;
id: string; id: string;
path?: string; path?: string;
description?: string | null;
}[]; }[];
}; };

View File

@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request";
import { secretSharingKeys } from "./queries"; import { secretSharingKeys } from "./queries";
import { import {
TCreatedSharedSecret, TCreatedSharedSecret,
TCreateSecretRequestRequestDTO,
TCreateSharedSecretRequest, TCreateSharedSecretRequest,
TDeleteSharedSecretRequest, TDeleteSecretRequestDTO,
TDeleteSharedSecretRequestDTO,
TRevealedSecretRequest,
TRevealSecretRequestValueRequest,
TSetSecretRequestValueRequest,
TSharedSecret TSharedSecret
} from "./types"; } from "./types";
@ -15,7 +20,7 @@ export const useCreateSharedSecret = () => {
return useMutation({ return useMutation({
mutationFn: async (inputData: TCreateSharedSecretRequest) => { mutationFn: async (inputData: TCreateSharedSecretRequest) => {
const { data } = await apiRequest.post<TCreatedSharedSecret>( const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing", "/api/v1/secret-sharing/shared",
inputData inputData
); );
return data; return data;
@ -30,7 +35,7 @@ export const useCreatePublicSharedSecret = () => {
return useMutation({ return useMutation({
mutationFn: async (inputData: TCreateSharedSecretRequest) => { mutationFn: async (inputData: TCreateSharedSecretRequest) => {
const { data } = await apiRequest.post<TCreatedSharedSecret>( const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing/public", "/api/v1/secret-sharing/shared/public",
inputData inputData
); );
return data; return data;
@ -40,12 +45,50 @@ export const useCreatePublicSharedSecret = () => {
}); });
}; };
export const useCreateSecretRequest = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (inputData: TCreateSecretRequestRequestDTO) => {
const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing/requests",
inputData
);
return data;
},
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSecretRequests() })
});
};
export const useSetSecretRequestValue = () => {
return useMutation({
mutationFn: async (inputData: TSetSecretRequestValueRequest) => {
const { data } = await apiRequest.post<TSharedSecret>(
`/api/v1/secret-sharing/requests/${inputData.id}/set-value`,
inputData
);
return data;
}
});
};
export const useRevealSecretRequestValue = () => {
return useMutation({
mutationFn: async (inputData: TRevealSecretRequestValueRequest) => {
const { data } = await apiRequest.post<TRevealedSecretRequest>(
`/api/v1/secret-sharing/requests/${inputData.id}/reveal-value`,
inputData
);
return data.secretRequest;
}
});
};
export const useDeleteSharedSecret = () => { export const useDeleteSharedSecret = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({ return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => { mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequestDTO) => {
const { data } = await apiRequest.delete<TSharedSecret>( const { data } = await apiRequest.delete<TSharedSecret>(
`/api/v1/secret-sharing/${sharedSecretId}` `/api/v1/secret-sharing/shared/${sharedSecretId}`
); );
return data; return data;
}, },
@ -53,3 +96,19 @@ export const useDeleteSharedSecret = () => {
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSharedSecrets() }) queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSharedSecrets() })
}); });
}; };
export const useDeleteSecretRequest = () => {
const queryClient = useQueryClient();
return useMutation<TSharedSecret, unknown, TDeleteSecretRequestDTO>({
mutationFn: async ({ secretRequestId }: TDeleteSecretRequestDTO) => {
const { data } = await apiRequest.delete<TSharedSecret>(
`/api/v1/secret-sharing/requests/${secretRequestId}`
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSecretRequests() });
}
});
};

View File

@ -2,16 +2,20 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TSharedSecret, TViewSharedSecretResponse } from "./types"; import { TGetSecretRequestByIdResponse, TSharedSecret, TViewSharedSecretResponse } from "./types";
export const secretSharingKeys = { export const secretSharingKeys = {
allSharedSecrets: () => ["sharedSecrets"] as const, allSharedSecrets: () => ["sharedSecrets"] as const,
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) => specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const, [...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
allSecretRequests: () => ["secretRequests"] as const,
specificSecretRequests: ({ offset, limit }: { offset: number; limit: number }) =>
[...secretSharingKeys.allSecretRequests(), { offset, limit }] as const,
getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [ getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
"shared-secret", "shared-secret",
arg arg
] ],
getSecretRequestById: (arg: { id: string }) => ["secret-request", arg] as const
}; };
export const useGetSharedSecrets = ({ export const useGetSharedSecrets = ({
@ -30,7 +34,7 @@ export const useGetSharedSecrets = ({
}); });
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>( const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
"/api/v1/secret-sharing/", "/api/v1/secret-sharing/shared",
{ {
params params
} }
@ -40,6 +44,29 @@ export const useGetSharedSecrets = ({
}); });
}; };
export const useGetSecretRequests = ({
offset = 0,
limit = 25
}: {
offset: number;
limit: number;
}) => {
return useQuery({
queryKey: secretSharingKeys.specificSecretRequests({ offset, limit }),
queryFn: async () => {
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
"/api/v1/secret-sharing/requests",
{
params: {
offset: String(offset),
limit: String(limit)
}
}
);
return data;
}
});
};
export const useGetActiveSharedSecretById = ({ export const useGetActiveSharedSecretById = ({
sharedSecretId, sharedSecretId,
hashedHex, hashedHex,
@ -53,7 +80,7 @@ export const useGetActiveSharedSecretById = ({
queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }), queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.post<TViewSharedSecretResponse>( const { data } = await apiRequest.post<TViewSharedSecretResponse>(
`/api/v1/secret-sharing/public/${sharedSecretId}`, `/api/v1/secret-sharing/shared/public/${sharedSecretId}`,
{ {
...(hashedHex && { hashedHex }), ...(hashedHex && { hashedHex }),
password password
@ -65,3 +92,16 @@ export const useGetActiveSharedSecretById = ({
enabled: Boolean(sharedSecretId) enabled: Boolean(sharedSecretId)
}); });
}; };
export const useGetSecretRequestById = ({ secretRequestId }: { secretRequestId: string }) => {
return useQuery({
queryKey: secretSharingKeys.getSecretRequestById({ id: secretRequestId }),
queryFn: async () => {
const { data } = await apiRequest.get<TGetSecretRequestByIdResponse>(
`/api/v1/secret-sharing/requests/${secretRequestId}`
);
return data.secretRequest;
}
});
};

View File

@ -6,13 +6,21 @@ export type TSharedSecret = {
updatedAt: Date; updatedAt: Date;
name: string | null; name: string | null;
lastViewedAt?: Date; lastViewedAt?: Date;
accessType: SecretSharingAccessType;
expiresAt: Date; expiresAt: Date;
expiresAfterViews: number | null; expiresAfterViews: number | null;
encryptedValue: string; encryptedValue: string;
encryptedSecret: string;
iv: string; iv: string;
tag: string; tag: string;
}; };
export type TRevealedSecretRequest = {
secretRequest: {
secretValue: string;
} & TSharedSecret;
};
export type TCreatedSharedSecret = { export type TCreatedSharedSecret = {
id: string; id: string;
}; };
@ -26,6 +34,21 @@ export type TCreateSharedSecretRequest = {
accessType?: SecretSharingAccessType; accessType?: SecretSharingAccessType;
}; };
export type TCreateSecretRequestRequestDTO = {
name?: string;
accessType?: SecretSharingAccessType;
expiresAt: Date;
};
export type TSetSecretRequestValueRequest = {
secretValue: string;
id: string;
};
export type TRevealSecretRequestValueRequest = {
id: string;
};
export type TViewSharedSecretResponse = { export type TViewSharedSecretResponse = {
isPasswordProtected: boolean; isPasswordProtected: boolean;
secret: { secret: {
@ -38,10 +61,27 @@ export type TViewSharedSecretResponse = {
}; };
}; };
export type TDeleteSharedSecretRequest = { export type TGetSecretRequestByIdResponse = {
secretRequest: {
isSecretValueSet: boolean;
accessType: SecretSharingAccessType;
requester: {
organizationName: string;
username: string;
firstName?: string;
lastName?: string;
};
};
};
export type TDeleteSharedSecretRequestDTO = {
sharedSecretId: string; sharedSecretId: string;
}; };
export type TDeleteSecretRequestDTO = {
secretRequestId: string;
};
export enum SecretSharingAccessType { export enum SecretSharingAccessType {
Anyone = "anyone", Anyone = "anyone",
Organization = "organization" Organization = "organization"

View File

@ -51,6 +51,7 @@ export type SecretV3RawSanitized = {
folderId?: string; folderId?: string;
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
secretMetadata?: { key: string; value: string }[]; secretMetadata?: { key: string; value: string }[];
isReminderEvent?: boolean;
}; };
export type SecretV3Raw = { export type SecretV3Raw = {
@ -100,6 +101,12 @@ export type SecretVersions = {
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
actor?: {
actorId?: string | null;
actorType?: string | null;
name?: string | null;
membershipId?: string | null;
} | null;
}; };
// dto // dto

View File

@ -2,13 +2,22 @@ import { useCallback, useMemo } from "react";
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types"; import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
type FolderNameAndDescription = {
name: string;
description?: string;
};
export const useFolderOverview = (folders: DashboardProjectSecretsOverview["folders"]) => { export const useFolderOverview = (folders: DashboardProjectSecretsOverview["folders"]) => {
const folderNames = useMemo(() => { const folderNamesAndDescriptions = useMemo(() => {
const names = new Set<string>(); const namesAndDescriptions = new Map<string, FolderNameAndDescription>();
folders?.forEach((folder) => { folders?.forEach((folder) => {
names.add(folder.name); if (!namesAndDescriptions.has(folder.name)) {
namesAndDescriptions.set(folder.name, { name: folder.name, description: folder.description });
}
}); });
return [...names];
return Array.from(namesAndDescriptions.values());
}, [folders]); }, [folders]);
const isFolderPresentInEnv = useCallback( const isFolderPresentInEnv = useCallback(
@ -31,7 +40,7 @@ export const useFolderOverview = (folders: DashboardProjectSecretsOverview["fold
[folders] [folders]
); );
return { folderNames, isFolderPresentInEnv, getFolderByNameAndEnv }; return { folderNamesAndDescriptions, isFolderPresentInEnv, getFolderByNameAndEnv };
}; };
export const useDynamicSecretOverview = ( export const useDynamicSecretOverview = (

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { faMagnifyingGlass, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faMagnifyingGlass, faUsers, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@ -9,7 +9,6 @@ import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
IconButton,
Input, Input,
Table, Table,
TableContainer, TableContainer,
@ -18,21 +17,33 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr Tr,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2"; } from "@app/components/v2";
import { useSubscription, useUser } from "@app/context"; import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks"; import { useDebounce, usePopUp } from "@app/hooks";
import { useAdminDeleteUser, useAdminGetUsers } from "@app/hooks/api"; import {
useAdminDeleteUser,
useAdminGetUsers,
useAdminGrantServerAdminAccess
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const deleteUserUpgradePlanMessage = "Deleting users via Admin UI";
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
const UserPanelTable = ({ const UserPanelTable = ({
handlePopUpOpen handlePopUpOpen
}: { }: {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeUser", "upgradePlan"]>, popUpName: keyof UsePopUpState<["removeUser", "upgradePlan", "upgradeToServerAdmin"]>,
data?: { data?: {
username: string; username: string;
id: string; id: string;
message?: string;
} }
) => void; ) => void;
}) => { }) => {
@ -87,22 +98,49 @@ const UserPanelTable = ({
<Td> <Td>
{userId !== id && ( {userId !== id && (
<div className="flex justify-end"> <div className="flex justify-end">
<IconButton <DropdownMenu>
size="lg" <DropdownMenuTrigger asChild className="rounded-lg">
colorSchema="danger" <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
variant="plain" <FontAwesomeIcon size="sm" icon={faEllipsis} />
ariaLabel="update" </div>
isDisabled={userId === id} </DropdownMenuTrigger>
onClick={() => { <DropdownMenuContent align="start" className="p-1">
if (!subscription?.instanceUserManagement) { <DropdownMenuItem
handlePopUpOpen("upgradePlan"); onClick={(e) => {
return; e.stopPropagation();
} if (!subscription?.instanceUserManagement) {
handlePopUpOpen("removeUser", { username, id }); handlePopUpOpen("upgradePlan", {
}} username,
> id,
<FontAwesomeIcon icon={faXmark} /> message: deleteUserUpgradePlanMessage
</IconButton> });
return;
}
handlePopUpOpen("removeUser", { username, id });
}}
>
Remove User
</DropdownMenuItem>
{!superAdmin && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
if (!subscription?.instanceUserManagement) {
handlePopUpOpen("upgradePlan", {
username,
id,
message: addServerAdminUpgradePlanMessage
});
return;
}
handlePopUpOpen("upgradeToServerAdmin", { username, id });
}}
>
Make User Server Admin
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
)} )}
</Td> </Td>
@ -134,10 +172,12 @@ const UserPanelTable = ({
export const UserPanel = () => { export const UserPanel = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeUser", "removeUser",
"upgradePlan" "upgradePlan",
"upgradeToServerAdmin"
] as const); ] as const);
const { mutateAsync: deleteUser } = useAdminDeleteUser(); const { mutateAsync: deleteUser } = useAdminDeleteUser();
const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess();
const handleRemoveUser = async () => { const handleRemoveUser = async () => {
const { id } = popUp?.removeUser?.data as { id: string; username: string }; const { id } = popUp?.removeUser?.data as { id: string; username: string };
@ -158,6 +198,25 @@ export const UserPanel = () => {
handlePopUpClose("removeUser"); handlePopUpClose("removeUser");
}; };
const handleGrantServerAdminAccess = async () => {
const { id } = popUp?.upgradeToServerAdmin?.data as { id: string; username: string };
try {
await grantAdminAccess(id);
createNotification({
type: "success",
text: "Successfully granted server admin access to user"
});
} catch {
createNotification({
type: "error",
text: "Error granting server admin access to user"
});
}
handlePopUpClose("upgradeToServerAdmin");
};
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4"> <div className="mb-4">
@ -173,10 +232,21 @@ export const UserPanel = () => {
onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)} onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)}
onDeleteApproved={handleRemoveUser} onDeleteApproved={handleRemoveUser}
/> />
<DeleteActionModal
isOpen={popUp.upgradeToServerAdmin.isOpen}
title={`Are you sure want to grant Server Admin permissions to ${
(popUp?.upgradeToServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
onChange={(isOpen) => handlePopUpToggle("upgradeToServerAdmin", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleGrantServerAdminAccess}
buttonText="Grant Access"
/>
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="Deleting users via Admin UI is only available on Infisical's Pro plan and above." text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`}
/> />
</div> </div>
); );

View File

@ -2,7 +2,7 @@ import { useEffect } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2"; import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2";
@ -36,6 +36,8 @@ export const SecretScanningPage = withPermission(
from: ROUTE_PATHS.Organization.SecretScanning.id from: ROUTE_PATHS.Organization.SecretScanning.id
}); });
const navigate = useNavigate();
const { control, watch } = useForm<SecretScanningFilterFormData>({ const { control, watch } = useForm<SecretScanningFilterFormData>({
resolver: zodResolver(secretScanningFilterFormSchema), resolver: zodResolver(secretScanningFilterFormSchema),
defaultValues: {} defaultValues: {}
@ -70,8 +72,11 @@ export const SecretScanningPage = withPermission(
const { mutateAsync: linkGitAppInstallationWithOrganization } = const { mutateAsync: linkGitAppInstallationWithOrganization } =
useLinkGitAppInstallationWithOrg(); useLinkGitAppInstallationWithOrg();
const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession(); const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession();
const { data: installationStatus, isPending: isSecretScanningInstatllationStatusLoading } = const {
useGetSecretScanningInstallationStatus(organizationId); data: installationStatus,
isPending: isSecretScanningInstatllationStatusLoading,
refetch: refetchSecretScanningInstallationStatus
} = useGetSecretScanningInstallationStatus(organizationId);
const integrationEnabled = const integrationEnabled =
!isSecretScanningInstatllationStatusLoading && installationStatus?.appInstallationCompleted; !isSecretScanningInstatllationStatusLoading && installationStatus?.appInstallationCompleted;
@ -86,7 +91,10 @@ export const SecretScanningPage = withPermission(
sessionId: String(queryParams.state) sessionId: String(queryParams.state)
}); });
if (isLinked) { if (isLinked) {
window.location.reload(); await navigate({
to: "/organization/secret-scanning"
});
refetchSecretScanningInstallationStatus();
} }
console.log("installation verification complete"); console.log("installation verification complete");

View File

@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { ShareSecretSection } from "./components"; import { ShareSecretSection } from "./ShareSecretSection";
export const SecretSharingPage = () => { export const SecretSharingPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();

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