Compare commits

...

174 Commits

Author SHA1 Message Date
39fdeabdea Merge pull request #2646 from Infisical/feat/sap-hana-dynamic-secrets
feat: SAP HANA dynamic secrets
2024-10-29 22:45:46 +08:00
25c26f2cde Merge pull request #2658 from Infisical/misc/add-missing-helm-updates-operator
misc: added helm related configs for operator
2024-10-29 09:53:17 -04:00
1ca8b9ba08 misc: install secret operator updates 2024-10-29 21:50:23 +08:00
14d9fe01e0 misc: updated chart 2024-10-29 21:46:17 +08:00
216810f289 misc: added helm related configs 2024-10-29 13:37:03 +08:00
f530b78eb8 Merge pull request #2652 from Infisical/feat/add-support-for-custom-ca
feat: add support for custom ca in k8 operator
2024-10-29 01:14:30 -04:00
c3809ed22b Merge branch 'feat/add-support-for-custom-ca' of https://github.com/Infisical/infisical into feat/add-support-for-custom-ca 2024-10-29 12:00:09 +08:00
9f85d8bba1 feat: added handling of empty ca 2024-10-29 11:59:41 +08:00
1056645ee3 fix small nit 2024-10-28 22:25:21 -04:00
5e9914b738 Merge pull request #2657 from Infisical/vmatsiiako--docs-patch-1
Update docker-swarm.mdx
2024-10-28 22:17:19 -04:00
1ea52e6a80 update chart version 2024-10-28 21:03:27 -04:00
20da697de8 rename change log file 2024-10-28 21:01:20 -04:00
16abf48081 add change log 2024-10-28 20:56:42 -04:00
e73ae485bc patch service account namespace 2024-10-28 20:32:38 -04:00
621f73e223 add support for variable init container img 2024-10-28 20:32:38 -04:00
93e69bd34e Merge pull request #2656 from scott-ray-wilson/insecure-context-banner
Feature: Display Warning Banner for Insecure Connection
2024-10-28 16:47:18 -07:00
e382135384 improvements: make banner full width and adjust icon/margins 2024-10-28 16:43:15 -07:00
f2a554b5fd Update docker-swarm.mdx 2024-10-28 16:16:36 -07:00
df5bdf3773 feature: display warning banner for insecure context 2024-10-28 16:00:40 -07:00
df46daf93d Merge pull request #2654 from scott-ray-wilson/kms-doc-fix
Docs: Correct KMS API Docs for Decrypt Endpoint
2024-10-28 11:18:43 -07:00
f82f7ae8d0 fix: correct api constant reference for kms docs 2024-10-28 11:10:04 -07:00
8536a1c987 Merge pull request #2653 from scott-ray-wilson/fix-copy-shared-secret-link
Fix: Copy Shared Secret Link to Clipboard on Generate
2024-10-28 10:56:19 -07:00
b3cf43b46d fix: copy shared secret link to clipboard on generate 2024-10-28 10:25:09 -07:00
9d4dbb63ae misc: updated go-sdk version 2024-10-28 21:49:34 +08:00
9c6f23fba6 misc: documentation and samples 2024-10-28 17:45:49 +08:00
babe483ca9 feat: add support for custom ca in k8 operator 2024-10-28 17:03:56 +08:00
38ede687cd Merge pull request #2650 from Infisical/revert-2649-revert-2603-feat/secret-reference-path-way
"feat: secret reference graph for understanding how its pulled""
2024-10-27 22:04:52 -04:00
5f465c4832 update env of prod eu ci 2024-10-26 21:19:53 -04:00
a0618086b0 update sts endpoint for eu ci 2024-10-26 20:38:37 -04:00
9a9bb4ca43 update eu deployment job 2024-10-26 18:45:12 -04:00
b68ddfae1b wait for gamma to be fully deployed 2024-10-26 17:57:28 -04:00
7646670378 update ci job names 2024-10-26 17:46:27 -04:00
d18be0f74c fix deployment ci 2024-10-26 17:44:05 -04:00
ec96db3503 Add EU support in deployment 2024-10-26 17:41:08 -04:00
bed620aad0 fix: downgrade collapsible version 2024-10-26 11:12:38 -07:00
2ddf75d2e6 Merge pull request #2636 from Infisical/daniel/envkey-refactor
feat: envkey import refactor
2024-10-26 14:01:32 -04:00
02d9dbb987 Revert "Revert "feat: secret reference graph for understanding how its pulled"" 2024-10-26 13:51:07 -04:00
0ed333c2b2 Merge pull request #2649 from Infisical/revert-2603-feat/secret-reference-path-way
Revert "feat: secret reference graph for understanding how its pulled"
2024-10-26 13:50:55 -04:00
55db45cd36 Revert "feat: secret reference graph for understanding how its pulled" 2024-10-26 13:50:44 -04:00
2d82273158 Merge pull request #2603 from akhilmhdh/feat/secret-reference-path-way
feat: secret reference graph for understanding how its pulled
2024-10-25 18:10:28 -04:00
b3e61f579d fix: prevent secret input from auto focusing to reveal value when opening dialog 2024-10-25 14:45:53 -07:00
d0bcbe15c6 fix: address minor typos 2024-10-25 14:40:04 -07:00
657130eb80 improvements: transitions and minor UI adjustments to secret reference tree 2024-10-25 14:23:42 -07:00
3841394eb7 misc: migrated existing to new helper 2024-10-26 03:18:57 +08:00
b1ba770a71 Update external-migration-fns.ts 2024-10-25 20:29:21 +04:00
3552119c7d misc: moved host validity check to helper 2024-10-25 22:49:57 +08:00
7a46725523 doc: added note for transaction 2024-10-25 22:45:06 +08:00
0515c994c7 Disable wait for gamma deployment 2024-10-25 10:26:21 -04:00
e0d0e22e39 Merge pull request #2647 from akhilmhdh/fix/recursive
fix: resolved recursive secret logic pulling secret from top
2024-10-25 10:11:53 -04:00
=
2f79ae42ab fix: resolved recursive secret logic pulling secret from top 2024-10-25 19:33:41 +05:30
3bc39c6cec feat: add usage of ca 2024-10-25 21:29:47 +08:00
b5b1e57fe7 doc: sap hana 2024-10-25 21:19:22 +08:00
1a5f66fe46 feat: added support for sap hana dynamic secrets 2024-10-25 20:33:23 +08:00
a01f235808 Merge pull request #2614 from nabilnalakath/update-docker-compose-redis-restart
Fix(docker-compose): prevent container restart failures on system reboot
2024-10-25 14:35:15 +05:30
=
b9a1629db0 feat: resolved merge conflicts 2024-10-25 14:17:17 +05:30
=
203422c131 feat: added reference tree to overview and secret main page, changed to better graph structure 2024-10-25 13:18:41 +05:30
=
35826c288e feat: changed to a simpler tree ascii ui 2024-10-25 13:11:15 +05:30
fae4e1fa55 minor improvements 2024-10-25 13:11:15 +05:30
=
8094ef607a feat: review nits and changes in loading 2024-10-25 13:11:15 +05:30
=
104bff0586 feat: secret reference graph for understanding how its pulled 2024-10-25 13:09:49 +05:30
0fb5fa0c8b Merge pull request #2644 from Infisical/doc/update-missing-github-app-permission
doc: added missing github app permission scope
2024-10-24 23:56:57 -04:00
f407022e16 Merge pull request #2615 from akhilmhdh/feat/permission-phase-2
Feat/permission phase 2
2024-10-24 15:36:20 -04:00
34d6525418 add permissions banner 2024-10-25 00:59:42 +05:30
=
911479baff feat: added banner in permission custom role page. 2024-10-25 00:59:42 +05:30
=
05bdbbf59d feat: removed folder striping in permission split project role migration 2024-10-25 00:59:42 +05:30
=
c8e47771d4 feat: refactored frontend form logic and added sort on list for roles, privileges 2024-10-25 00:59:41 +05:30
=
e0cbcb0318 feat: resolved merge issues 2024-10-25 00:59:41 +05:30
=
f8d65f44e3 feat: resolved ts fail in license check 2024-10-25 00:59:41 +05:30
=
58ce623a2c feat: removed console and resolved drift in tf due to folder removing in v1 2024-10-25 00:59:41 +05:30
=
7ae28596ec feat: added missing remove identity, user handler, changed title to duration for access type 2024-10-25 00:59:41 +05:30
833398ef39 improvement: add priv conflict info and improve back button 2024-10-25 00:59:41 +05:30
4e6ebcc8d9 improvement: fix variable name 2024-10-25 00:59:41 +05:30
ce8689f568 improvement: make identity add priv edit consistent with member 2024-10-25 00:59:40 +05:30
e9ab19b7f9 resolve rebase 2024-10-25 00:59:40 +05:30
=
f2b852a09e feat: removed form in member and identity table, and added ellipsis for noting icons 2024-10-25 00:59:40 +05:30
a1c2bc695c docs: add example temporal duration for temporary range 2024-10-25 00:59:40 +05:30
00573ebfda docs: add missing periods, standardize ID and fix typos to api constants 2024-10-25 00:59:40 +05:30
=
3b2b8ca013 feat: completed all nit changes in review 2024-10-25 00:59:40 +05:30
=
2afc6b133e feat: resolved role issue in ui 2024-10-25 00:59:40 +05:30
=
b6a1ab2376 feat: completed identity project detail screen 2024-10-25 00:59:40 +05:30
=
d03f890471 feat: completed new user detail page screen 2024-10-25 00:59:39 +05:30
=
5ef81cd935 feat: added role modify popup 2024-10-25 00:59:39 +05:30
=
3e8f1d8de7 feat: role list section completed with delete 2024-10-25 00:59:39 +05:30
=
558a809b4c feat: made v2 project role router use projectid instead of projectslug 2024-10-25 00:59:39 +05:30
=
a749e70815 feat: update user additional privilege router to new one 2024-10-25 00:59:39 +05:30
=
6f44f3ae21 feat: added backend endpoint for new identitiy additional privilege permission system 2024-10-25 00:59:39 +05:30
=
b062ca3075 feat: resolved identity privilege removed folder, dynamic secret and secret import permission 2024-10-25 00:59:38 +05:30
=
a1397f0a66 fix: resolved edge case in folder empty action 2024-10-25 00:59:38 +05:30
=
91c11d61f1 Revert "Revert "feat: added filter folder to remove read only in migration""
This reverts commit 78f668bd7f.
2024-10-25 00:59:38 +05:30
=
93218d5a3f Revert "Revert "Permission phase 2""
This reverts commit 8b9244b079.
2024-10-25 00:59:38 +05:30
5f2144eca5 doc: added missing github app permission scope 2024-10-25 00:43:40 +08:00
45b9de63f0 Merge pull request #2643 from Infisical/daniel/fix-cleanup-job-fail
fix: `timestamp out of range` error during daily cleanup
2024-10-24 19:52:12 +04:00
114966ded4 fix: LEAST() to fix TTL's with high values causing timestamp out of range 2024-10-24 19:40:20 +04:00
71081d8e9a Merge pull request #2620 from Infisical/daniel/operator-auth-refresh
feat: automatic k8 operator token refreshing
2024-10-24 10:42:46 -04:00
dad3d50f3e Merge pull request #2642 from Infisical/daniel/fix-aws-local-login
fix(cli): aws-iam local authentication
2024-10-24 17:48:42 +04:00
e5ca5d3da2 Update test-TestUniversalAuth_SecretsGetWrongEnvironment 2024-10-24 17:45:00 +04:00
301cd54dc3 chore: bumped go sdk version 2024-10-24 17:33:51 +04:00
593bda8bc6 Merge pull request #2633 from Infisical/feat/azure-app-configuration
feat: azure app configuration integration
2024-10-24 19:37:38 +08:00
4db79edf19 misc: addressed review comments 2024-10-24 19:20:21 +08:00
e3a356cda9 updated go sdk 2024-10-24 14:47:00 +04:00
521b24debf Merge pull request #2639 from Infisical/feat/add-assume-role-support-for-aws-parameter-store
feat: add assume role support for aws parameter store
2024-10-24 02:48:29 +08:00
d6ffd4fa5f fix: block precedence & root env priority 2024-10-23 17:23:17 +04:00
ca3b64bf6c Merge pull request #2637 from mtariqsajid/patch-1
jenkins integration is now available
2024-10-23 08:32:37 -04:00
b7e48fd556 feat: add assume role support for aws parameter store 2024-10-23 19:56:47 +08:00
c01ea048ce Merge pull request #2638 from scott-ray-wilson/fix-region-display
Fix: refine check of when to display region select
2024-10-22 23:58:15 -04:00
7e7d9a2bd5 fix: refine check of when to display region select 2024-10-22 20:56:10 -07:00
782e3a8985 jenkins integration is now available 2024-10-23 01:25:13 +05:00
1c32dd5d8a Update external-migration-types.ts 2024-10-22 22:37:33 +04:00
8497ac831f Merge pull request #2635 from Infisical/feat/allow-approvers-to-bypass-secret-change-requests
feat: allow approvers to bypass secret change requests
2024-10-22 22:28:52 +04:00
e5821122d5 Merge pull request #2597 from Infisical/feat/moved-mfa-to-org-level
feat: moved mfa to org level
2024-10-22 14:14:48 -04:00
c183ef2b4f feat: envkey import refactor 2024-10-22 22:09:33 +04:00
340693cfcd feat: allow approvers to bypass secret change requests 2024-10-23 01:54:40 +08:00
014b9585e0 Merge pull request #2634 from Infisical/azure-permission-docs
Add permission note for Azure Key Vault (KV) integration documentation
2024-10-22 09:48:25 -07:00
67373b0883 Add permission note for azure kv integration 2024-10-22 09:43:36 -07:00
2101040a77 misc: updated e2e 2024-10-23 00:13:10 +08:00
2e2fea304b Merge remote-tracking branch 'origin/main' into feat/moved-mfa-to-org-level 2024-10-23 00:04:09 +08:00
571709370d misc: addressed ux issues 2024-10-23 00:00:15 +08:00
e1dbe769a8 doc: added documentation for azure app configuration 2024-10-22 21:41:44 +08:00
e7e0d84c8e feat: azure app configuration integration 2024-10-22 19:41:10 +08:00
4c2ed1cc8b Merge pull request #2631 from scott-ray-wilson/fix-uw-secret-overview-overflow
Fix: cap width for tw container size on uw monitors
2024-10-22 00:48:48 -04:00
067b0f4232 fix: cap width for tw container size on uw monitors 2024-10-21 20:58:28 -07:00
6ed786e6d0 Merge pull request #2627 from Infisical/daniel/go-sdk-docs-update
docs: go SDK refreshing docs
2024-10-21 19:23:00 -04:00
d187cc3d4d Merge pull request #2622 from Infisical/daniel/error-context
feat: more contextual not found errors
2024-10-21 19:09:12 -04:00
614e4934a2 Merge pull request #2630 from scott-ray-wilson/incorrect-import-value-display-fix
Fix: Correct Secret Value Override Display on Overview Table
2024-10-21 14:18:43 -04:00
14e92f895a fix: only override secret value/id by key if same environment 2024-10-21 10:55:39 -07:00
0a38374a73 Merge pull request #2629 from Infisical/daniel/cli-snapshot-update
fix: cli snapshot test error message change
2024-10-21 21:29:34 +04:00
ec3b94a335 fix: snapshot error message change 2024-10-21 21:27:16 +04:00
ca0241bb51 improvement: improve empty table labeling for org group/users 2024-10-21 10:20:43 -07:00
7403385e7c fix: fix select all rows checkbox being checked if no folders/secrets 2024-10-21 09:45:56 -07:00
b6955d0e9b Update external-migration-queue.ts 2024-10-21 20:39:46 +04:00
f4ba441ec3 feat: envkey data migration refactor 2024-10-21 20:39:08 +04:00
2cd1141a65 Merge pull request #2610 from scott-ray-wilson/group-tables-fixes/improvements
Fix: Group Tables/Pagination Fixes and Improvements
2024-10-21 09:39:00 -07:00
256627b2cc Update go.mdx 2024-10-21 20:38:32 +04:00
fd7e196f8b Merge pull request #2628 from Infisical/misc/export-org-data-feature
feat: add migration script to migrate org
2024-10-21 10:16:41 -04:00
212748f140 misc: added cleanup of global/instance-level resources 2024-10-21 21:19:55 +08:00
b61582a60e Merge remote-tracking branch 'origin/main' into misc/export-org-data-feature 2024-10-21 19:04:02 +08:00
9ca8da152b Update go.mdx 2024-10-21 12:08:33 +04:00
c5aa1b8664 Merge pull request #2626 from Infisical/vmatsiiako-patch-docsimage-1
Update group-mappings.mdx
2024-10-20 21:18:48 -04:00
90dbb417ac Update group-mappings.mdx 2024-10-20 18:17:20 -07:00
7fb3076238 fix: added sdk context support 2024-10-19 08:40:02 +04:00
946651496f Merge pull request #2623 from Infisical/daniel/rate-limit-error
fix: better rate limit errors
2024-10-19 07:25:46 +04:00
5a8ac850b5 fix: variable naming 2024-10-19 06:41:29 +04:00
77a88f1575 feat: better rate limit errors 2024-10-19 06:35:49 +04:00
be00d13a46 Merge pull request #2621 from scott-ray-wilson/improve-overview-table-overflow
Improvement: Cap Expanded Secret View Width when Overview Table Overflows
2024-10-18 19:14:42 -04:00
84814a0012 improvement: improve handling of expanded secret when table overflows 2024-10-18 16:06:25 -07:00
a0865cda2e fix: enable sdk silent mode 2024-10-19 02:59:29 +04:00
de03692469 Merge pull request #2600 from scott-ray-wilson/select-all-secrets-page
Feat: Select All Rows for Secrets Tables
2024-10-18 18:30:35 -04:00
fb2d3e4eb7 Merge pull request #2618 from scott-ray-wilson/scim-group-mapping-docs
Docs: SCIM Group Mapping and SCIM/Organization Doc Improvements
2024-10-18 18:06:22 -04:00
29150e809d Merge pull request #2617 from Infisical/misc/allow-secret-scanning-whitelist
misc: added secret scanning whitelist configuration
2024-10-18 18:03:54 -04:00
e18a606b23 improvements: adjust UI for alignment and remove checkbox separator 2024-10-18 14:47:31 -07:00
67708411cd update tooltip for k8 2024-10-18 17:43:37 -04:00
1e7b1ccf22 feat: automatic token refreshing 2024-10-19 01:38:11 +04:00
3e4bd28916 Merge pull request #2619 from scott-ray-wilson/fix-default-tag-color
Fix: Set Default Value for Color in Tags Modal
2024-10-18 14:12:34 -07:00
a2e16370fa fix: set default value for color in tags modal 2024-10-18 14:06:38 -07:00
d677654311 improvement: org user groups tables search fixed and col sort added and group add users pagination fixed and search improved to include first and last name 2024-10-18 13:20:17 -07:00
903fac1005 misc: added infisical cli to docker and fixed redirect 2024-10-19 03:18:13 +08:00
ff045214d6 improve readability 2024-10-18 11:59:23 -07:00
57dcf5ab28 docs: scim group mapping and scim/org improvements 2024-10-18 11:57:36 -07:00
959a5ec55b misc: added secret scanning whitelist conig 2024-10-19 01:59:45 +08:00
b22a93a175 Merge pull request #2604 from akhilmhdh/feat/org-kms-ui
feat: added organization kms in org role permission section
2024-10-18 21:59:56 +05:30
5debeb421d Merge remote-tracking branch 'origin/main' into feat/moved-mfa-to-org-level 2024-10-18 20:07:59 +08:00
25b30e441a misc: added missing enforcement checks 2024-10-18 19:51:31 +08:00
0f314c45b4 Fix(docker-compose): prevent container restart failures on system reboot
- Added restart policies to ensure reliable service restarts for Redis and PostgreSQL.
2024-10-18 12:26:35 +05:30
d7d88f3356 Merge pull request #2613 from Infisical/vmatsiiako-patch-scim-docs
Update azure.mdx
2024-10-17 21:50:00 -07:00
dbaef9d227 Update azure.mdx 2024-10-17 21:42:45 -07:00
8eb668cd72 misc: removed remaining mfa handling 2024-10-18 03:26:48 +08:00
bb079b3e46 misc: updated cli interactive to support mfa in org select 2024-10-18 01:59:24 +08:00
d94b4b2a3c feat: select all on page for secrets tables and fix multipage select behavior for actions 2024-10-17 10:17:23 -07:00
=
9d90c35629 feat: added organization kms in org role permission section 2024-10-17 20:26:22 +05:30
7a77dc7343 feat: added mfa popup for all select org 2024-10-17 22:29:38 +08:00
bd1ed2614e feat: added enforceMfa toggle for orgs 2024-10-17 03:02:26 +08:00
9192c5caa2 feat: created reusable mfa flow 2024-10-17 02:01:22 +08:00
8da2213bf1 misc: removed mfa from existing login 2024-10-16 22:44:04 +08:00
9f6d837a9b feat: add migration script to migrate org 2024-10-07 17:28:32 +05:30
339 changed files with 14174 additions and 5106 deletions

View File

@ -7,12 +7,12 @@ permissions:
jobs:
infisical-tests:
name: Run tests before deployment
name: Integration tests
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-image:
name: Build backend image
name: Build
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
@ -104,8 +104,8 @@ jobs:
cluster: infisical-gamma-stage
wait-for-service-stability: true
production-postgres-deployment:
name: Deploy to production
production-us:
name: US production deploy
runs-on: ubuntu-latest
needs: [gamma-deployment]
environment:
@ -159,3 +159,54 @@ jobs:
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true
production-eu:
name: EU production deploy
runs-on: ubuntu-latest
needs: [production-us]
environment:
name: production-eu
steps:
- uses: twingate/github-action@v1
with:
service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.eu-central-1.amazonaws.com
aws-region: eu-central-1
role-to-assume: arn:aws:iam::345594589636:role/gha-make-prod-deployment
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-core-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true

View File

@ -95,6 +95,10 @@ RUN mkdir frontend-build
# Production stage
FROM base AS production
RUN apk add --upgrade --no-cache ca-certificates
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.31.1 && apk add --no-cache git
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user

View File

@ -39,8 +39,6 @@ describe("Login V1 Router", async () => {
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("mfaEnabled");
expect(payload).toHaveProperty("token");
expect(payload.mfaEnabled).toBeFalsy();
});
});

View File

@ -56,7 +56,10 @@ describe("Secret expansion", () => {
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
for (const secret of secrets) {
// eslint-disable-next-line no-await-in-loop
await createSecretV2(secret);
}
const expandedSecret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
@ -123,7 +126,10 @@ describe("Secret expansion", () => {
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
for (const secret of secrets) {
// eslint-disable-next-line no-await-in-loop
await createSecretV2(secret);
}
const expandedSecret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
@ -190,7 +196,11 @@ describe("Secret expansion", () => {
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
for (const secret of secrets) {
// eslint-disable-next-line no-await-in-loop
await createSecretV2(secret);
}
const secretImportFromProdToDev = await createSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
@ -275,7 +285,11 @@ describe("Secret expansion", () => {
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
for (const secret of secrets) {
// eslint-disable-next-line no-await-in-loop
await createSecretV2(secret);
}
const secretImportFromProdToDev = await createSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,

File diff suppressed because it is too large Load Diff

View File

@ -58,6 +58,7 @@
"migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"migrate:org": "tsx ./scripts/migrate-organization.ts",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
@ -160,6 +161,7 @@
"google-auth-library": "^9.9.0",
"googleapis": "^137.1.0",
"handlebars": "^4.7.8",
"hdb": "^0.19.10",
"ioredis": "^5.3.2",
"jmespath": "^0.16.0",
"jsonwebtoken": "^9.0.2",

View File

@ -0,0 +1,84 @@
/* eslint-disable */
import promptSync from "prompt-sync";
import { execSync } from "child_process";
import path from "path";
import { existsSync } from "fs";
const prompt = promptSync({
sigint: true
});
const exportDb = () => {
const exportHost = prompt("Enter your Postgres Host to migrate from: ");
const exportPort = prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432";
const exportUser = prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical";
const exportPassword = prompt("Enter your Postgres Password to migrate from: ");
const exportDatabase = prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical";
// we do not include the audit_log and secret_sharing entries
execSync(
`PGDATABASE="${exportDatabase}" PGPASSWORD="${exportPassword}" PGHOST="${exportHost}" PGPORT=${exportPort} PGUSER=${exportUser} pg_dump infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
__dirname,
"../src/db/dump.sql"
)}`,
{ stdio: "inherit" }
);
};
const importDbForOrg = () => {
const importHost = prompt("Enter your Postgres Host to migrate to: ");
const importPort = prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432";
const importUser = prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical";
const importPassword = prompt("Enter your Postgres Password to migrate to: ");
const importDatabase = prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical";
const orgId = prompt("Enter the organization ID to migrate: ");
if (!existsSync(path.join(__dirname, "../src/db/dump.sql"))) {
console.log("File not found, please export the database first.");
return;
}
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -f ${path.join(
__dirname,
"../src/db/dump.sql"
)}`
);
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
);
// delete global/instance-level resources not relevant to the organization to migrate
// users
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
);
// identities
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
);
// reset slack configuration in superAdmin
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
);
console.log("Organization migrated successfully.");
};
const main = () => {
const action = prompt(
"Enter the action to perform\n 1. Export from existing instance.\n 2. Import org to instance.\n \n Action: "
);
if (action === "1") {
exportDb();
} else if (action === "2") {
importDbForOrg();
} else {
console.log("Invalid action");
}
};
main();

View File

@ -13,6 +13,7 @@ import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secr
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
@ -177,6 +178,7 @@ declare module "fastify" {
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
identityProjectAdditionalPrivilegeV2: TIdentityProjectAdditionalPrivilegeV2ServiceFactory;
secretSharing: TSecretSharingServiceFactory;
rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory;

4
backend/src/@types/hdb.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "hdb" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, the function returns `any`.
function createClient(options): any;
}

View File

@ -4,27 +4,40 @@ import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const hasEncryptedSecret = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSecret");
const hasIdentifier = await knex.schema.hasColumn(TableName.SecretSharing, "identifier");
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.string("iv").nullable().alter();
t.string("tag").nullable().alter();
t.string("encryptedValue").nullable().alter();
t.binary("encryptedSecret").nullable();
if (!hasEncryptedSecret) {
t.binary("encryptedSecret").nullable();
}
t.string("hashedHex").nullable().alter();
t.string("identifier", 64).nullable();
t.unique("identifier");
t.index("identifier");
if (!hasIdentifier) {
t.string("identifier", 64).nullable();
t.unique("identifier");
t.index("identifier");
}
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasEncryptedSecret = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSecret");
const hasIdentifier = await knex.schema.hasColumn(TableName.SecretSharing, "identifier");
if (await knex.schema.hasTable(TableName.SecretSharing)) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.dropColumn("encryptedSecret");
if (hasEncryptedSecret) {
t.dropColumn("encryptedSecret");
}
t.dropColumn("identifier");
if (hasIdentifier) {
t.dropColumn("identifier");
}
});
}
}

View File

@ -7,15 +7,18 @@ export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.KmsKey)) {
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId");
// drop constraint if exists (won't exist if rolled back, see below)
await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex);
// projectId for CMEK functionality
await knex.schema.alterTable(TableName.KmsKey, (table) => {
table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE");
if (!hasProjectId) {
table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE");
}
if (hasOrgId) {
if (hasOrgId && hasSlug) {
table.unique(["orgId", "projectId", "slug"]);
}
@ -30,6 +33,7 @@ export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.KmsKey)) {
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name");
const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId");
// remove projectId for CMEK functionality
await knex.schema.alterTable(TableName.KmsKey, (table) => {
@ -40,7 +44,9 @@ export async function down(knex: Knex): Promise<void> {
if (hasOrgId) {
table.dropUnique(["orgId", "projectId", "slug"]);
}
table.dropColumn("projectId");
if (hasProjectId) {
table.dropColumn("projectId");
}
});
}
}

View File

@ -0,0 +1,101 @@
/* eslint-disable no-await-in-loop */
import { packRules, unpackRules } from "@casl/ability/extra";
import { Knex } from "knex";
import {
backfillPermissionV1SchemaToV2Schema,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TableName } from "../schemas";
const CHUNK_SIZE = 1000;
export async function up(knex: Knex): Promise<void> {
const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version");
if (!hasVersion) {
await knex.schema.alterTable(TableName.ProjectRoles, (t) => {
t.integer("version").defaultTo(1).notNullable();
});
const docs = await knex(TableName.ProjectRoles).select("*");
const updatedDocs = docs
.filter((i) => {
const permissionString = JSON.stringify(i.permissions || []);
return (
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
!permissionString.includes(ProjectPermissionSub.DynamicSecrets)
);
})
.map((el) => ({
...el,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions), true)))
}));
if (updatedDocs.length) {
for (let i = 0; i < updatedDocs.length; i += CHUNK_SIZE) {
const chunk = updatedDocs.slice(i, i + CHUNK_SIZE);
await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge();
}
}
// secret permission is split into multiple ones like secrets, folders, imports and dynamic-secrets
// so we just find all the privileges with respective mapping and map it as needed
const identityPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select("*");
const updatedIdentityPrivilegesDocs = identityPrivileges
.filter((i) => {
const permissionString = JSON.stringify(i.permissions || []);
return (
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
!permissionString.includes(ProjectPermissionSub.DynamicSecrets) &&
!permissionString.includes(ProjectPermissionSub.SecretFolders)
);
})
.map((el) => ({
...el,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
}));
if (updatedIdentityPrivilegesDocs.length) {
for (let i = 0; i < updatedIdentityPrivilegesDocs.length; i += CHUNK_SIZE) {
const chunk = updatedIdentityPrivilegesDocs.slice(i, i + CHUNK_SIZE);
await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge();
}
}
const userPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select("*");
const updatedUserPrivilegeDocs = userPrivileges
.filter((i) => {
const permissionString = JSON.stringify(i.permissions || []);
return (
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
!permissionString.includes(ProjectPermissionSub.DynamicSecrets) &&
!permissionString.includes(ProjectPermissionSub.SecretFolders)
);
})
.map((el) => ({
...el,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
}));
if (docs.length) {
for (let i = 0; i < updatedUserPrivilegeDocs.length; i += CHUNK_SIZE) {
const chunk = updatedUserPrivilegeDocs.slice(i, i + CHUNK_SIZE);
await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge();
}
}
}
}
export async function down(knex: Knex): Promise<void> {
const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version");
if (hasVersion) {
await knex.schema.alterTable(TableName.ProjectRoles, (t) => {
t.dropColumn("version");
});
// permission change can be ignored
}
}

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.Organization, "enforceMfa"))) {
await knex.schema.alterTable(TableName.Organization, (tb) => {
tb.boolean("enforceMfa").defaultTo(false).notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Organization, "enforceMfa")) {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.dropColumn("enforceMfa");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SamlConfig, "orgId")) {
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
t.dropForeign("orgId");
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SamlConfig, "orgId")) {
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
t.dropForeign("orgId");
t.foreign("orgId").references("id").inTable(TableName.Organization);
});
}
}

View File

@ -20,7 +20,8 @@ export const OrganizationsSchema = z.object({
scimEnabled: z.boolean().default(false).nullable().optional(),
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
defaultMembershipRole: z.string().default("member")
defaultMembershipRole: z.string().default("member"),
enforceMfa: z.boolean().default(false)
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@ -165,7 +165,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
username: z.string().optional().describe(GROUPS.LIST_USERS.username)
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search)
}),
response: {
200: z.object({

View File

@ -1,9 +1,9 @@
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@ -79,7 +79,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false,
permissions: JSON.stringify(packRules(permission))
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: backfillPermissionV1SchemaToV2Schema(permission)
});
return { privilege };
}
@ -159,7 +161,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: true,
permissions: JSON.stringify(packRules(permission))
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: backfillPermissionV1SchemaToV2Schema(permission)
});
return { privilege };
}
@ -244,7 +248,13 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: req.body.projectSlug,
data: {
...updatedInfo,
permissions: permission ? JSON.stringify(packRules(permission)) : undefined
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: permission
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
backfillPermissionV1SchemaToV2Schema(permission)
: undefined
}
});
return { privilege };

View File

@ -81,9 +81,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(registerGroupRouter, { prefix: "/groups" });
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
await server.register(registerUserAdditionalPrivilegeRouter, { prefix: "/user-project-additional-privilege" });
await server.register(
async (privilegeRouter) => {
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" });
},
{ prefix: "/additional-privilege" }

View File

@ -3,12 +3,16 @@ import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission";
import {
backfillPermissionV1SchemaToV2Schema,
ProjectPermissionV1Schema
} from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
@ -43,11 +47,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.CREATE.permissions)
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
}),
response: {
200: z.object({
role: SanitizedRoleSchema
role: SanitizedRoleSchemaV1
})
}
},
@ -58,12 +62,16 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
projectSlug: req.params.projectSlug,
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
},
data: {
...req.body,
permissions: JSON.stringify(packRules(req.body.permissions))
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
}
});
return { role };
}
});
@ -103,11 +111,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}),
response: {
200: z.object({
role: SanitizedRoleSchema
role: SanitizedRoleSchemaV1
})
}
},
@ -118,11 +126,12 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
projectSlug: req.params.projectSlug,
roleId: req.params.roleId,
data: {
...req.body,
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
permissions: req.body.permissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined
}
});
return { role };
@ -148,7 +157,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
role: SanitizedRoleSchema
role: SanitizedRoleSchemaV1
})
}
},
@ -159,7 +168,6 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
projectSlug: req.params.projectSlug,
roleId: req.params.roleId
});
return { role };
@ -195,7 +203,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
projectSlug: req.params.projectSlug
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
}
});
return { roles };
}
@ -214,7 +225,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
role: SanitizedRoleSchema
role: SanitizedRoleSchemaV1
})
}
},
@ -225,9 +236,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
projectSlug: req.params.projectSlug,
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
},
roleSlug: req.params.slug
});
return { role };
}
});

View File

@ -2,6 +2,8 @@ import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -23,6 +25,13 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const appCfg = getConfig();
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
throw new BadRequestError({
message: "Secret scanning is temporarily unavailable."
});
}
const session = await server.services.secretScanning.createInstallationSession({
actor: req.permission.type,
actorId: req.permission.id,
@ -30,6 +39,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId
});
return session;
}
});

View File

@ -2,17 +2,18 @@ import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/permanent",
url: "/",
method: "POST",
config: {
rateLimit: writeLimit
@ -31,66 +32,30 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
})
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
}),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const privilege = await server.services.projectUserAdditionalPrivilege.create({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false,
permissions: JSON.stringify(req.body.permissions)
});
return { privilege };
}
});
server.route({
method: "POST",
url: "/temporary",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
type: z.discriminatedUnion("isTemporary", [
z.object({
isTemporary: z.literal(false)
}),
z.object({
isTemporary: z.literal(true),
temporaryMode: z
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime)
})
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
temporaryMode: z
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime)
])
}),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
privilege: SanitizedUserProjectAdditionalPrivilegeSchema
})
}
},
@ -101,10 +66,10 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`,
isTemporary: true,
permissions: JSON.stringify(req.body.permissions)
projectMembershipId: req.body.projectMembershipId,
...req.body.type,
slug: req.body.slug || slugify(alphaNumericNanoId(8)),
permissions: req.body.permissions
});
return { privilege };
}
@ -131,24 +96,31 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
message: "Slug must be a valid slug"
})
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime)
permissions: ProjectPermissionV2Schema.array()
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
z.object({
isTemporary: z.literal(true).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => typeof val === "undefined" || ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime)
})
])
})
.partial(),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
privilege: SanitizedUserProjectAdditionalPrivilegeSchema
})
}
},
@ -160,7 +132,12 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined,
...req.body.type,
permissions: req.body.permissions
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
req.body.permissions
: undefined,
privilegeId: req.params.privilegeId
});
return { privilege };
@ -179,7 +156,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
}),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
privilege: SanitizedUserProjectAdditionalPrivilegeSchema
})
}
},
@ -208,7 +185,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
}),
response: {
200: z.object({
privileges: ProjectUserAdditionalPrivilegeSchema.array()
privileges: SanitizedUserProjectAdditionalPrivilegeSchema.omit({ permissions: true }).array()
})
}
},
@ -233,11 +210,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
},
schema: {
params: z.object({
privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.GET_BY_PRIVILEGEID.privilegeId)
privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.GET_BY_PRIVILEGE_ID.privilegeId)
}),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
privilege: SanitizedUserProjectAdditionalPrivilegeSchema
})
}
},

View File

@ -0,0 +1,305 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: "Add an additional privilege for identity.",
security: [
{
bearerAuth: []
}
],
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
type: z.discriminatedUnion("isTemporary", [
z.object({
isTemporary: z.literal(false)
}),
z.object({
isTemporary: z.literal(true),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.temporaryAccessStartTime)
})
])
}),
response: {
200: z.object({
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilegeV2.create({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
projectId: req.body.projectId,
identityId: req.body.identityId,
...req.body.type,
slug: req.body.slug || slugify(alphaNumericNanoId(8)),
permissions: req.body.permissions
});
return { privilege };
}
});
server.route({
method: "PATCH",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update a specific identity privilege.",
security: [
{
bearerAuth: []
}
],
params: z.object({
id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id)
}),
body: z.object({
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array()
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
z.object({
isTemporary: z.literal(true).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => typeof val === "undefined" || ms(val) > 0, "Temporary range must be a positive number")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.temporaryAccessStartTime)
})
])
}),
response: {
200: z.object({
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilegeV2.updateById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.id,
data: {
...req.body,
...req.body.type,
permissions: req.body.permissions || undefined
}
});
return { privilege };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete the specified identity privilege.",
security: [
{
bearerAuth: []
}
],
params: z.object({
id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.DELETE.id)
}),
response: {
200: z.object({
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilegeV2.deleteById({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.id
});
return { privilege };
}
});
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
description: "Retrieve details of a specific privilege by id.",
security: [
{
bearerAuth: []
}
],
params: z.object({
id: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.GET_BY_ID.id)
}),
response: {
200: z.object({
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilegeV2.getPrivilegeDetailsById({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
id: req.params.id
});
return { privilege };
}
});
server.route({
method: "GET",
url: "/slug/:privilegeSlug",
config: {
rateLimit: readLimit
},
schema: {
description: "Retrieve details of a specific privilege by slug.",
security: [
{
bearerAuth: []
}
],
params: z.object({
privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.GET_BY_SLUG.slug)
}),
querystring: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.GET_BY_SLUG.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.GET_BY_SLUG.projectSlug)
}),
response: {
200: z.object({
privilege: SanitizedIdentityPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilegeV2.getPrivilegeDetailsBySlug({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
slug: req.params.privilegeSlug,
...req.query
});
return { privilege };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List privileges for the specified identity by project.",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.LIST.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.LIST.projectId)
}),
response: {
200: z.object({
privileges: SanitizedIdentityPrivilegeSchema.omit({ permissions: true }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privileges = await server.services.identityProjectAdditionalPrivilegeV2.listIdentityProjectPrivileges({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
return {
privileges
};
}
});
};

View File

@ -0,0 +1,16 @@
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerProjectRoleRouter } from "./project-role-router";
export const registerV2EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
await server.register(
async (projectRouter) => {
await projectRouter.register(registerProjectRoleRouter);
},
{ prefix: "/workspace" }
);
await server.register(registerIdentityProjectAdditionalPrivilegeRouter, {
prefix: "/identity-project-additional-privilege"
});
};

View File

@ -0,0 +1,242 @@
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/roles",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
}),
body: z.object({
slug: z
.string()
.toLowerCase()
.trim()
.min(1)
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
}),
response: {
200: z.object({
role: SanitizedRoleSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.ID,
projectId: req.params.projectId
},
data: {
...req.body,
permissions: JSON.stringify(packRules(req.body.permissions))
}
});
return { role };
}
});
server.route({
method: "PATCH",
url: "/:projectId/roles/:roleId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectId),
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}),
body: z.object({
slug: z
.string()
.toLowerCase()
.trim()
.optional()
.describe(PROJECT_ROLE.UPDATE.slug)
.refine(
(val) =>
typeof val === "undefined" ||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
message: "Slug must be a valid"
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}),
response: {
200: z.object({
role: SanitizedRoleSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
roleId: req.params.roleId,
data: {
...req.body,
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
}
});
return { role };
}
});
server.route({
method: "DELETE",
url: "/:projectId/roles/:roleId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_ROLE.DELETE.projectId),
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
}),
response: {
200: z.object({
role: SanitizedRoleSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const role = await server.services.projectRole.deleteRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
roleId: req.params.roleId
});
return { role };
}
});
server.route({
method: "GET",
url: "/:projectId/roles",
config: {
rateLimit: readLimit
},
schema: {
description: "List project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_ROLE.LIST.projectId)
}),
response: {
200: z.object({
roles: ProjectRolesSchema.omit({ permissions: true }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const roles = await server.services.projectRole.listRoles({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.ID,
projectId: req.params.projectId
}
});
return { roles };
}
});
server.route({
method: "GET",
url: "/:projectId/roles/slug/:roleSlug",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectId),
roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
}),
response: {
200: z.object({
role: SanitizedRoleSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const role = await server.services.projectRole.getRoleBySlug({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.ID,
projectId: req.params.projectId
},
roleSlug: req.params.roleSlug
});
return { role };
}
});
};

View File

@ -14,7 +14,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const accessApprovalPolicyFindQuery = async (
tx: Knex,
filter: TFindFilter<TAccessApprovalPolicies>,
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
customFilter?: {
policyId?: string;
}

View File

@ -1,36 +0,0 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ActorType } from "@app/services/auth/auth-type";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TIsApproversValid } from "./access-approval-policy-types";
export const isApproversValid = async ({
userIds,
projectId,
orgId,
envSlug,
actorAuthMethod,
secretPath,
permissionService
}: TIsApproversValid) => {
try {
for await (const userId of userIds) {
const { permission: approverPermission } = await permissionService.getProjectPermission(
ActorType.USER,
userId,
projectId,
actorAuthMethod,
orgId
);
ForbiddenError.from(approverPermission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
);
}
} catch {
return false;
}
return true;
};

View File

@ -11,7 +11,6 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGroupDALFactory } from "../group/group-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { isApproversValid } from "./access-approval-policy-fns";
import {
ApproverType,
TCreateAccessApprovalPolicy,
@ -124,7 +123,9 @@ export const accessApprovalPolicyServiceFactory = ({
const verifyAllApprovers = [...approverUserIds];
for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
usersPromises.push(
groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }).then((group) => group.members)
);
}
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
@ -132,22 +133,6 @@ export const accessApprovalPolicyServiceFactory = ({
.map((user) => user.id);
verifyAllApprovers.push(...verifyGroupApprovers);
const approversValid = await isApproversValid({
projectId: project.id,
orgId: actorOrgId,
envSlug: environment,
secretPath,
actorAuthMethod,
permissionService,
userIds: verifyAllApprovers
});
if (!approversValid) {
throw new BadRequestError({
message: "One or more approvers doesn't have access to be specified secret path"
});
}
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create(
{
@ -291,22 +276,6 @@ export const accessApprovalPolicyServiceFactory = ({
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
}
const approversValid = await isApproversValid({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: userApproverIds
});
if (!approversValid) {
throw new BadRequestError({
message: "One or more approvers doesn't have access to be specified secret path"
});
}
await accessApprovalPolicyApproverDAL.insertMany(
userApproverIds.map((userId) => ({
approverUserId: userId,
@ -317,41 +286,6 @@ export const accessApprovalPolicyServiceFactory = ({
}
if (groupApprovers) {
const usersPromises: Promise<
{
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
}
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
.filter((user) => user.isPartOfGroup)
.map((user) => user.id);
const approversValid = await isApproversValid({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: verifyGroupApprovers
});
if (!approversValid) {
throw new BadRequestError({
message: "One or more approvers doesn't have access to be specified secret path"
});
}
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,

View File

@ -17,7 +17,6 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { isApproversValid } from "../access-approval-policy/access-approval-policy-fns";
import { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
@ -78,7 +77,6 @@ export const accessApprovalRequestServiceFactory = ({
permissionService,
accessApprovalRequestDAL,
accessApprovalRequestReviewerDAL,
projectMembershipDAL,
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
additionalPrivilegeDAL,
@ -151,10 +149,12 @@ export const accessApprovalRequestServiceFactory = ({
const groupUsers = (
await Promise.all(
approverGroupIds.map((groupApproverId) =>
groupDAL.findAllGroupPossibleMembers({
orgId: actorOrgId,
groupId: groupApproverId
})
groupDAL
.findAllGroupPossibleMembers({
orgId: actorOrgId,
groupId: groupApproverId
})
.then((group) => group.members)
)
)
).flat();
@ -329,22 +329,6 @@ export const accessApprovalRequestServiceFactory = ({
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
}
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
const approversValid = await isApproversValid({
projectId: accessApprovalRequest.projectId,
orgId: actorOrgId,
envSlug: accessApprovalRequest.environment,
secretPath: accessApprovalRequest.policy.secretPath!,
actorAuthMethod,
permissionService,
userIds: [reviewerProjectMembership.userId]
});
if (!approversValid) {
throw new ForbiddenRequestError({ message: "You don't have access to approve this request" });
}
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });

View File

@ -4,7 +4,10 @@ import ms from "ms";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -72,8 +75,8 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId);
@ -151,8 +154,8 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId);
@ -230,8 +233,8 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -299,8 +302,8 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -341,8 +344,8 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);

View File

@ -0,0 +1,20 @@
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
export const verifyHostInputValidity = (host: string) => {
const appCfg = getConfig();
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
if (
appCfg.isCloud &&
// localhost
// internal ips
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
throw new BadRequestError({ message: "Invalid db host" });
}
};

View File

@ -3,7 +3,10 @@ import { ForbiddenError, subject } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
@ -77,8 +80,8 @@ export const dynamicSecretServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.CreateRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId);
@ -148,8 +151,8 @@ export const dynamicSecretServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId);
@ -231,8 +234,8 @@ export const dynamicSecretServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -291,8 +294,12 @@ export const dynamicSecretServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -340,8 +347,8 @@ export const dynamicSecretServiceFactory = ({
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
)
);
}
@ -380,8 +387,8 @@ export const dynamicSecretServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -428,8 +435,8 @@ export const dynamicSecretServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -471,8 +478,8 @@ export const dynamicSecretServiceFactory = ({
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
)
);
}

View File

@ -2,10 +2,9 @@ import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
const generatePassword = () => {
@ -19,23 +18,8 @@ const generateUsername = () => {
export const ElasticSearchProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
) {
throw new BadRequestError({ message: "Invalid db host" });
}
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -9,6 +9,7 @@ import { MongoAtlasProvider } from "./mongo-atlas";
import { MongoDBProvider } from "./mongo-db";
import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis";
import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database";
export const buildDynamicSecretProviders = () => ({
@ -22,5 +23,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider()
[DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider()
});

View File

@ -166,6 +166,17 @@ export const DynamicSecretMongoDBSchema = z.object({
)
});
export const DynamicSecretSapHanaSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
username: z.string().trim(),
password: z.string().trim(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional()
});
export const AzureEntraIDSchema = z.object({
tenantId: z.string().trim().min(1),
userId: z.string().trim().min(1),
@ -196,7 +207,8 @@ export enum DynamicSecretProviders {
MongoDB = "mongo-db",
RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id",
Ldap = "ldap"
Ldap = "ldap",
SapHana = "sap-hana"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@ -204,6 +216,7 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),

View File

@ -2,10 +2,9 @@ import { MongoClient } from "mongodb";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
@ -19,22 +18,8 @@ const generateUsername = () => {
export const MongoDBProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
if (
appCfg.isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -3,12 +3,11 @@ import https from "https";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
@ -79,23 +78,8 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa
export const RabbitMqProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
) {
throw new BadRequestError({ message: "Invalid db host" });
}
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -3,11 +3,10 @@ import { Redis } from "ioredis";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
@ -51,22 +50,8 @@ const executeTransactions = async (connection: Redis, commands: string[]): Promi
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
throw new BadRequestError({ message: "Invalid db host" });
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -0,0 +1,174 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import handlebars from "handlebars";
import hdb from "hdb";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const SapHanaProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
const client = hdb.createClient({
host: providerInputs.host,
port: providerInputs.port,
user: providerInputs.username,
password: providerInputs.password,
...(providerInputs.ca
? {
ca: providerInputs.ca
}
: {})
});
await new Promise((resolve, reject) => {
client.connect((err: any) => {
if (err) {
return reject(err);
}
if (client.readyState) {
return resolve(true);
}
reject(new Error("SAP HANA client not ready"));
});
});
return client;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const testResult: boolean = await new Promise((resolve, reject) => {
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
if (err) {
reject();
}
resolve(true);
});
});
return testResult;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername();
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
const client = await getClient(providerInputs);
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration
});
const queries = creationStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
}
resolve(true);
});
});
}
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, username: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
const queries = revokeStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
}
resolve(true);
});
});
}
return { entityId: username };
};
const renew = async (inputs: unknown, username: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
try {
const expiration = new Date(expireAt).toISOString();
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
const queries = renewStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
}
resolve(true);
});
});
}
} finally {
client.disconnect();
}
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -3,11 +3,9 @@ import knex from "knex";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
@ -29,27 +27,8 @@ const generateUsername = (provider: SqlProviders) => {
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (
providerInputs.host === "localhost" ||
providerInputs.host === "127.0.0.1" ||
// database infisical uses
dbHost === providerInputs.host
)
throw new BadRequestError({ message: "Invalid db host" });
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -65,16 +65,18 @@ export const groupDALFactory = (db: TDbClient) => {
groupId,
offset = 0,
limit,
username
username, // depreciated in favor of search
search
}: {
orgId: string;
groupId: string;
offset?: number;
limit?: number;
username?: string;
search?: string;
}) => {
try {
let query = db
const query = db
.replicaNode()(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
@ -92,31 +94,39 @@ export const groupDALFactory = (db: TDbClient) => {
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId")
db.ref("id").withSchema(TableName.Users).as("userId"),
db.raw(`count(*) OVER() as total_count`)
)
.where({ isGhost: false })
.offset(offset);
.offset(offset)
.orderBy("firstName", "asc");
if (limit) {
query = query.limit(limit);
void query.limit(limit);
}
if (username) {
query = query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
if (search) {
void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike '%${search}%'`);
} else if (username) {
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
}
const members = await query;
return members.map(
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
id: userId,
email,
username: memberUsername,
firstName,
lastName,
isPartOfGroup: !!memberGroupId
})
);
return {
members: members.map(
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
id: userId,
email,
username: memberUsername,
firstName,
lastName,
isPartOfGroup: !!memberGroupId
})
),
// @ts-expect-error col select is raw and not strongly typed
totalCount: Number(members?.[0]?.total_count ?? 0)
};
} catch (error) {
throw new DatabaseError({ error, name: "Find all org members" });
}

View File

@ -221,7 +221,8 @@ export const groupServiceFactory = ({
actor,
actorId,
actorAuthMethod,
actorOrgId
actorOrgId,
search
}: TListGroupUsersDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
@ -244,17 +245,16 @@ export const groupServiceFactory = ({
message: `Failed to find group with ID ${id}`
});
const users = await groupDAL.findAllGroupPossibleMembers({
const { members, totalCount } = await groupDAL.findAllGroupPossibleMembers({
orgId: group.orgId,
groupId: group.id,
offset,
limit,
username
username,
search
});
const count = await orgDAL.countAllOrgMembers(group.orgId);
return { users, totalCount: count };
return { users: members, totalCount };
};
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {

View File

@ -38,6 +38,7 @@ export type TListGroupUsersDTO = {
offset: number;
limit: number;
username?: string;
search?: string;
} & TGenericPermission;
export type TAddUserToGroupDTO = {

View File

@ -0,0 +1,12 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityProjectAdditionalPrivilegeV2DALFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeV2DALFactory
>;
export const identityProjectAdditionalPrivilegeV2DALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege);
return orm;
};

View File

@ -0,0 +1,343 @@
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import ms from "ms";
import { TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal";
import {
IdentityProjectAdditionalPrivilegeTemporaryMode,
TCreateIdentityPrivilegeDTO,
TDeleteIdentityPrivilegeByIdDTO,
TGetIdentityPrivilegeDetailsByIdDTO,
TGetIdentityPrivilegeDetailsBySlugDTO,
TListIdentityPrivilegesDTO,
TUpdateIdentityPrivilegeByIdDTO
} from "./identity-project-additional-privilege-v2-types";
type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeV2ServiceFactory
>;
export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
identityProjectAdditionalPrivilegeDAL,
identityProjectDAL,
projectDAL,
permissionService
}: TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep) => {
const create = async ({
slug,
actor,
actorId,
projectId,
actorOrgId,
identityId,
permissions: customPermission,
actorAuthMethod,
...dto
}: TCreateIdentityPrivilegeDTO) => {
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (existingSlug) throw new BadRequestError({ message: "Additional privilege with provided slug already exists" });
const packedPermission = JSON.stringify(packRules(customPermission));
if (!dto.isTemporary) {
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: packedPermission
});
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: packedPermission,
isTemporary: true,
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: dto.temporaryRange,
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
});
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const updateById = async ({
id,
data,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TUpdateIdentityPrivilegeByIdDTO) => {
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id);
if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` });
const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId });
if (!identityProjectMembership)
throw new NotFoundError({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
if (data?.slug) {
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug: data.slug,
projectMembershipId: identityProjectMembership.id
});
if (existingSlug && existingSlug.id !== identityPrivilege.id)
throw new BadRequestError({ message: "Additional privilege with provided slug already exists" });
}
const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined;
if (isTemporary) {
const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
slug: data.slug,
permissions: packedPermission,
isTemporary: data.isTemporary,
temporaryRange: data.temporaryRange,
temporaryMode: data.temporaryMode,
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
slug: data.slug,
permissions: packedPermission,
isTemporary: false,
temporaryAccessStartTime: null,
temporaryAccessEndTime: null,
temporaryRange: null,
temporaryMode: null
});
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const deleteById = async ({ actorId, id, actor, actorOrgId, actorAuthMethod }: TDeleteIdentityPrivilegeByIdDTO) => {
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id);
if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` });
const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId });
if (!identityProjectMembership)
throw new NotFoundError({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
return {
...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions)
};
};
const getPrivilegeDetailsById = async ({
id,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TGetIdentityPrivilegeDetailsByIdDTO) => {
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id);
if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` });
const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId });
if (!identityProjectMembership)
throw new NotFoundError({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
return {
...identityPrivilege,
permissions: unpackPermissions(identityPrivilege.permissions)
};
};
const getPrivilegeDetailsBySlug = async ({
identityId,
slug,
projectSlug,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TGetIdentityPrivilegeDetailsBySlugDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug ${slug} not found` });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" });
return {
...identityPrivilege,
permissions: unpackPermissions(identityPrivilege.permissions)
};
};
const listIdentityProjectPrivileges = async ({
identityId,
actorOrgId,
actor,
actorId,
actorAuthMethod,
projectId
}: TListIdentityPrivilegesDTO) => {
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find(
{
projectMembershipId: identityProjectMembership.id
},
{ sort: [[`${TableName.IdentityProjectAdditionalPrivilege}.slug` as "slug", "asc"]] }
);
return identityPrivileges;
};
return {
getPrivilegeDetailsById,
getPrivilegeDetailsBySlug,
listIdentityProjectPrivileges,
create,
updateById,
deleteById
};
};

View File

@ -0,0 +1,55 @@
import { TProjectPermission } from "@app/lib/types";
import { TProjectPermissionV2Schema } from "../permission/project-permission";
export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TCreateIdentityPrivilegeDTO = {
permissions: TProjectPermissionV2Schema[];
identityId: string;
projectId: string;
slug: string;
} & (
| {
isTemporary: false;
}
| {
isTemporary: true;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
) &
Omit<TProjectPermission, "projectId">;
export type TUpdateIdentityPrivilegeByIdDTO = { id: string } & Omit<TProjectPermission, "projectId"> & {
data: Partial<{
permissions: TProjectPermissionV2Schema[];
slug: string;
isTemporary: boolean;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}>;
};
export type TDeleteIdentityPrivilegeByIdDTO = Omit<TProjectPermission, "projectId"> & {
id: string;
};
export type TGetIdentityPrivilegeDetailsByIdDTO = Omit<TProjectPermission, "projectId"> & {
id: string;
};
export type TListIdentityPrivilegesDTO = Omit<TProjectPermission, "projectId"> & {
identityId: string;
projectId: string;
};
export type TGetIdentityPrivilegeDetailsBySlugDTO = Omit<TProjectPermission, "projectId"> & {
slug: string;
identityId: string;
projectSlug: string;
};

View File

@ -1,10 +1,10 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { z } from "zod";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@ -32,16 +32,6 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeServiceFactory
>;
// TODO(akhilmhdh): move this to more centralized
export const UnpackedPermissionSchema = z.object({
subject: z
.union([z.string().min(1), z.string().array()])
.transform((el) => (typeof el !== "string" ? el[0] : el))
.optional(),
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
conditions: z.unknown().optional()
});
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
@ -80,14 +70,18 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
@ -97,11 +91,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
});
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
const packedPermission = JSON.stringify(packRules(customPermission));
if (!dto.isTemporary) {
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: customPermission
permissions: packedPermission
});
return {
...additionalPrivilege,
@ -113,7 +108,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: customPermission,
permissions: packedPermission,
isTemporary: true,
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: dto.temporaryRange,
@ -152,14 +147,19 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
@ -182,23 +182,29 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
}
const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined;
if (isTemporary) {
const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
...data,
slug: data.slug,
permissions: packedPermission,
isTemporary: data.isTemporary,
temporaryRange: data.temporaryRange,
temporaryMode: data.temporaryMode,
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
...data,
slug: data.slug,
permissions: packedPermission,
isTemporary: false,
temporaryAccessStartTime: null,
temporaryAccessEndTime: null,
@ -207,7 +213,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
});
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
@ -289,7 +294,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
@ -335,7 +340,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
});
return identityPrivileges.map((el) => ({
...el,
permissions: unpackPermissions(el.permissions)
}));
};

View File

@ -1,11 +1,13 @@
import { TProjectPermission } from "@app/lib/types";
import { TProjectPermissionV2Schema } from "../permission/project-permission";
export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TCreateIdentityPrivilegeDTO = {
permissions: unknown;
permissions: TProjectPermissionV2Schema[];
identityId: string;
projectSlug: string;
slug: string;
@ -27,7 +29,7 @@ export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; pr
"projectId"
> & {
data: Partial<{
permissions: unknown;
permissions: TProjectPermissionV2Schema[];
slug: string;
isTemporary: boolean;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;

View File

@ -46,7 +46,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
writeLimit: 200,
secretsLimit: 40
},
pkiEst: false
pkiEst: false,
enforceMfa: false
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@ -64,6 +64,7 @@ export type TFeatureSet = {
secretsLimit: number;
};
pkiEst: boolean;
enforceMfa: boolean;
};
export type TOrgPlansTableDTO = {

View File

@ -67,7 +67,7 @@ export const permissionServiceFactory = ({
throw new NotFoundError({ name: "OrgRoleInvalid", message: `Organization role '${role}' not found` });
}
})
.reduce((curr, prev) => prev.concat(curr), []);
.reduce((prev, curr) => prev.concat(curr), []);
return createMongoAbility<OrgPermissionSet>(rules, {
conditionsMatcher
@ -98,7 +98,7 @@ export const permissionServiceFactory = ({
});
}
})
.reduce((curr, prev) => prev.concat(curr), []);
.reduce((prev, curr) => prev.concat(curr), []);
return rules;
};

View File

@ -11,8 +11,8 @@ export enum PermissionConditionOperators {
}
export const PermissionConditionSchema = {
[PermissionConditionOperators.$IN]: z.string().min(1).array(),
[PermissionConditionOperators.$ALL]: z.string().min(1).array(),
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
[PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(),
[PermissionConditionOperators.$REGEX]: z
.string()
.min(1)

View File

@ -1,9 +1,8 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import { TableName } from "@app/db/schemas";
import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
@ -23,6 +22,14 @@ export enum ProjectPermissionCmekActions {
Decrypt = "decrypt"
}
export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential",
EditRootCredential = "edit-root-credential",
DeleteRootCredential = "delete-root-credential",
Lease = "lease"
}
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
@ -38,6 +45,8 @@ export enum ProjectPermissionSub {
Project = "workspace",
Secrets = "secrets",
SecretFolders = "secret-folders",
SecretImports = "secret-imports",
DynamicSecrets = "dynamic-secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
@ -54,19 +63,8 @@ export enum ProjectPermissionSub {
export type SecretSubjectFields = {
environment: string;
secretPath: string;
// secretName: string;
// secretTags: string[];
};
export const CaslSecretsV2SubjectKnexMapper = (field: string) => {
switch (field) {
case "secretName":
return `${TableName.SecretV2}.key`;
case "secretTags":
return `${TableName.SecretTag}.slug`;
default:
break;
}
secretName?: string;
secretTags?: string[];
};
export type SecretFolderSubjectFields = {
@ -74,6 +72,16 @@ export type SecretFolderSubjectFields = {
secretPath: string;
};
export type DynamicSecretSubjectFields = {
environment: string;
secretPath: string;
};
export type SecretImportSubjectFields = {
environment: string;
secretPath: string;
};
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
@ -86,6 +94,20 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
)
]
| [
ProjectPermissionDynamicSecretActions,
(
| ProjectPermissionSub.DynamicSecrets
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretImports
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SecretImportSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
@ -120,7 +142,9 @@ const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTI
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
const SecretConditionSchema = z
// akhilmhdh: don't modify this for v2
// if you want to update create a new schema
const SecretConditionV1Schema = z
.object({
environment: z.union([
z.string(),
@ -146,16 +170,50 @@ const SecretConditionSchema = z
})
.partial();
export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
const SecretConditionV2Schema = z
.object({
environment: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]),
secretPath: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]),
secretName: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]),
secretTags: z
.object({
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
})
.partial();
const GeneralPermissionSchema = [
z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
@ -259,7 +317,7 @@ export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to. "),
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
@ -288,26 +346,90 @@ export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
"Describe what action an entity can take."
)
})
];
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionV1Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
"Describe what action an entity can take."
)
}),
...GeneralPermissionSchema
]);
export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionV2Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionV1Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretImports).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionV1Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.DynamicSecrets).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionV1Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema
]);
export type TProjectPermissionV2Schema = z.infer<typeof ProjectPermissionV2Schema>;
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
// Admins get full access to everything
[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Member,
@ -339,6 +461,17 @@ const buildAdminPermissionRules = () => {
);
});
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
@ -370,6 +503,34 @@ const buildMemberPermissionRules = () => {
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretFolders
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretImports
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
@ -493,6 +654,9 @@ const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
@ -595,17 +759,63 @@ export const isAtLeastAsPrivilegedWorkspace = (
};
/* eslint-enable */
export const SecretV2SubjectFieldMapper = (arg: string) => {
switch (arg) {
case "environment":
return null;
case "secretPath":
return null;
case "secretName":
return `${TableName.SecretV2}.key`;
case "secretTags":
return `${TableName.SecretTag}.slug`;
default:
throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` });
export const backfillPermissionV1SchemaToV2Schema = (
data: z.infer<typeof ProjectPermissionV1Schema>[],
dontRemoveReadFolderPermission?: boolean
) => {
let formattedData = UnpackedPermissionSchema.array().parse(data);
const secretSubjects = formattedData.filter((el) => el.subject === ProjectPermissionSub.Secrets);
// this means the folder permission as readonly is set
const hasReadOnlyFolder = formattedData.filter((el) => el.subject === ProjectPermissionSub.SecretFolders);
const secretImportPolicies = secretSubjects.map(({ subject, ...el }) => ({
...el,
subject: ProjectPermissionSub.SecretImports as const
}));
const secretFolderPolicies = secretSubjects
.map(({ subject, ...el }) => ({
...el,
// read permission is not needed anymore
action: el.action.filter((caslAction) => caslAction !== ProjectPermissionActions.Read),
subject: ProjectPermissionSub.SecretFolders
}))
.filter((el) => el.action?.length > 0);
const dynamicSecretPolicies = secretSubjects.map(({ subject, ...el }) => {
const action = el.action.map((e) => {
switch (e) {
case ProjectPermissionActions.Edit:
return ProjectPermissionDynamicSecretActions.EditRootCredential;
case ProjectPermissionActions.Create:
return ProjectPermissionDynamicSecretActions.CreateRootCredential;
case ProjectPermissionActions.Delete:
return ProjectPermissionDynamicSecretActions.DeleteRootCredential;
case ProjectPermissionActions.Read:
return ProjectPermissionDynamicSecretActions.ReadRootCredential;
default:
return ProjectPermissionDynamicSecretActions.ReadRootCredential;
}
});
return {
...el,
action: el.action.includes(ProjectPermissionActions.Edit)
? [...action, ProjectPermissionDynamicSecretActions.Lease]
: action,
subject: ProjectPermissionSub.DynamicSecrets
};
});
if (!dontRemoveReadFolderPermission) {
formattedData = formattedData.filter((i) => i.subject !== ProjectPermissionSub.SecretFolders);
}
return formattedData.concat(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
secretImportPolicies,
dynamicSecretPolicies,
hasReadOnlyFolder.length ? [] : secretFolderPolicies
);
};

View File

@ -1,11 +1,16 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
import {
ProjectUserAdditionalPrivilegeTemporaryMode,
@ -26,6 +31,11 @@ export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType<
typeof projectUserAdditionalPrivilegeServiceFactory
>;
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
export const projectUserAdditionalPrivilegeServiceFactory = ({
projectUserAdditionalPrivilegeDAL,
projectMembershipDAL,
@ -43,7 +53,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
}: TCreateUserPrivilegeDTO) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID '${projectMembershipId}' not found` });
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` });
const { permission } = await permissionService.getProjectPermission(
actor,
@ -53,22 +63,41 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission(
ActorType.USER,
projectMembership.userId,
projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug,
projectId: projectMembership.projectId,
userId: projectMembership.userId
});
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
if (existingSlug)
throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` });
const packedPermission = JSON.stringify(packRules(customPermission));
if (!dto.isTemporary) {
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
userId: projectMembership.userId,
projectId: projectMembership.projectId,
slug,
permissions: customPermission
permissions: packedPermission
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
@ -76,14 +105,17 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
projectId: projectMembership.projectId,
userId: projectMembership.userId,
slug,
permissions: customPermission,
permissions: packedPermission,
isTemporary: true,
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: dto.temporaryRange,
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const updateById = async ({
@ -96,7 +128,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
}: TUpdateUserPrivilegeDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID '${privilegeId}' not found` });
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
@ -116,6 +148,20 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission(
ActorType.USER,
projectMembership.userId,
projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
if (dto?.slug) {
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
@ -124,36 +170,50 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
projectId: projectMembership.projectId
});
if (existingSlug && existingSlug.id !== userPrivilege.id)
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
throw new BadRequestError({ message: `Additional privilege with provided slug ${dto.slug} already exists` });
}
const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary;
const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions));
if (isTemporary) {
const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime;
const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange;
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
...dto,
slug: dto.slug,
permissions: packedPermission,
isTemporary: dto.isTemporary,
temporaryRange: dto.temporaryRange,
temporaryMode: dto.temporaryMode,
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
...dto,
slug: dto.slug,
permissions: packedPermission,
isTemporary: false,
temporaryAccessStartTime: null,
temporaryAccessEndTime: null,
temporaryRange: null,
temporaryMode: null
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID '${privilegeId}' not found` });
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
@ -174,7 +234,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
return deletedPrivilege;
return {
...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions)
};
};
const getPrivilegeDetailsById = async ({
@ -186,7 +249,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
}: TGetUserPrivilegeDetailsDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID '${privilegeId}' not found` });
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
@ -206,7 +269,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return userPrivilege;
return {
...userPrivilege,
permissions: unpackPermissions(userPrivilege.permissions)
};
};
const listPrivileges = async ({
@ -218,7 +284,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
}: TListUserPrivilegesDTO) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID '${projectMembershipId}' not found` });
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
@ -229,10 +295,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({
userId: projectMembership.userId,
projectId: projectMembership.projectId
});
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find(
{
userId: projectMembership.userId,
projectId: projectMembership.projectId
},
{ sort: [[`${TableName.ProjectUserAdditionalPrivilege}.slug` as "slug", "asc"]] }
);
return userPrivileges;
};

View File

@ -1,18 +1,20 @@
import { TProjectPermission } from "@app/lib/types";
import { TProjectPermissionV2Schema } from "../permission/project-permission";
export enum ProjectUserAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TCreateUserPrivilegeDTO = (
| {
permissions: unknown;
permissions: TProjectPermissionV2Schema[];
projectMembershipId: string;
slug: string;
isTemporary: false;
}
| {
permissions: unknown;
permissions: TProjectPermissionV2Schema[];
projectMembershipId: string;
slug: string;
isTemporary: true;
@ -25,7 +27,7 @@ export type TCreateUserPrivilegeDTO = (
export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit<TProjectPermission, "projectId"> &
Partial<{
permissions: unknown;
permissions: TProjectPermissionV2Schema[];
slug: string;
isTemporary: boolean;
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;

View File

@ -834,10 +834,12 @@ export const scimServiceFactory = ({
});
}
const users = await groupDAL.findAllGroupPossibleMembers({
orgId: group.orgId,
groupId: group.id
});
const users = await groupDAL
.findAllGroupPossibleMembers({
orgId: group.orgId,
groupId: group.id
})
.then((g) => g.members);
const orgMemberships = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,

View File

@ -14,7 +14,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const secretApprovalPolicyFindQuery = (
tx: Knex,
filter: TFindFilter<TSecretApprovalPolicies>,
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: {
sapId?: string;
}

View File

@ -1,4 +1,4 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ForbiddenError } from "@casl/ability";
import picomatch from "picomatch";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@ -356,17 +356,8 @@ export const secretApprovalPolicyServiceFactory = ({
environment,
secretPath
}: TGetBoardSapDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
);
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
return getSecretApprovalPolicy(projectId, environment, secretPath);
};

View File

@ -43,7 +43,7 @@ import {
fnSecretBulkDelete as fnSecretV2BridgeBulkDelete,
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
getAllSecretReferences as getAllSecretReferencesV2Bridge
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
@ -531,11 +531,11 @@ export const secretApprovalRequestServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
references: el.encryptedValue
? getAllNestedSecretReferencesV2Bridge(
? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({
cipherTextBlob: el.encryptedValue
}).toString()
)
).nestedReferences
: [],
type: SecretType.Shared
})),
@ -555,11 +555,11 @@ export const secretApprovalRequestServiceFactory = ({
? {
encryptedValue: el.encryptedValue as Buffer,
references: el.encryptedValue
? getAllNestedSecretReferencesV2Bridge(
? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({
cipherTextBlob: el.encryptedValue
}).toString()
)
).nestedReferences
: []
}
: {};
@ -1143,10 +1143,6 @@ export const secretApprovalRequestServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@ -1309,7 +1305,24 @@ export const secretApprovalRequestServiceFactory = ({
const tagIds = unique(Object.values(commitTagIds).flat());
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" });
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
const tagsGroupById = groupBy(tags, (i) => i.id);
commits.forEach((commit) => {
let action = ProjectPermissionActions.Create;
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit;
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete;
ForbiddenError.from(permission).throwUnlessCan(
action,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: commit.key,
secretTags: commitTagIds?.[commit.key]?.map((secretTagId) => tagsGroupById[secretTagId][0].slug)
})
);
});
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
const doc = await secretApprovalRequestDAL.create(

View File

@ -28,8 +28,7 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
import {
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
getAllSecretReferences
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
@ -253,11 +252,12 @@ export const secretReplicationServiceFactory = ({
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
const sourceImportedSecrets = await fnSecretsV2FromImports({
allowedImports: sourceSecretImports,
secretImports: sourceSecretImports,
secretDAL: secretV2BridgeDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: () => true
});
// secrets that gets replicated across imports
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
@ -419,7 +419,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
};
})
});
@ -445,7 +445,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
}
};
})
@ -694,7 +694,7 @@ export const secretReplicationServiceFactory = ({
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: getAllNestedSecretReferences(doc.secretValue)
references: getAllSecretReferences(doc.secretValue).nestedReferences
};
})
});
@ -730,7 +730,7 @@ export const secretReplicationServiceFactory = ({
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: getAllNestedSecretReferences(doc.secretValue)
references: getAllSecretReferences(doc.secretValue).nestedReferences
}
};
})

View File

@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv";
import { ProjectVersion } from "@app/db/schemas";
import { ProjectVersion, TableName } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
@ -103,13 +103,14 @@ export const secretRotationServiceFactory = ({
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const project = await projectDAL.findById(projectId);
const shouldUseBridge = project.version === ProjectVersion.V3;
if (shouldUseBridge) {
const selectedSecrets = await secretV2BridgeDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
$in: { [`${TableName.SecretV2}.id` as "id"]: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` });

View File

@ -1,6 +1,6 @@
import { ProbotOctokit } from "probot";
import { OrgMembershipRole } from "@app/db/schemas";
import { OrgMembershipRole, TableName } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
@ -61,7 +61,7 @@ export const secretScanningQueueFactory = ({
const getOrgAdminEmails = async (organizationId: string) => {
// get emails of admins
const adminsOfWork = await orgMemberDAL.findMembership({
orgId: organizationId,
[`${TableName.Organization}.id` as string]: organizationId,
role: OrgMembershipRole.Admin
});
return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string);

View File

@ -90,7 +90,7 @@ export const secretScanningServiceFactory = ({
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
if (!appCfg.DISABLE_SECRET_SCANNING) {
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
await Promise.all(
repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({
@ -164,7 +164,7 @@ export const secretScanningServiceFactory = ({
});
if (!installationLink) return;
if (!appCfg.DISABLE_SECRET_SCANNING) {
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
await secretScanningQueue.startPushEventScan({
commits,
pusher: { name: pusher.name, email: pusher.email },

View File

@ -5,30 +5,31 @@ export const GROUPS = {
role: "The role of the group to create."
},
UPDATE: {
id: "The id of the group to update",
id: "The ID of the group to update.",
name: "The new name of the group to update to.",
slug: "The new slug of the group to update to.",
role: "The new role of the group to update to."
},
DELETE: {
id: "The id of the group to delete",
slug: "The slug of the group to delete"
id: "The ID of the group to delete.",
slug: "The slug of the group to delete."
},
LIST_USERS: {
id: "The id of the group to list users for",
id: "The ID of the group to list users for.",
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
limit: "The number of users to return.",
username: "The username to search for."
username: "The username to search for.",
search: "The text string that user email or name will be filtered by."
},
ADD_USER: {
id: "The id of the group to add the user to.",
id: "The ID of the group to add the user to.",
username: "The username of the user to add to the group."
},
GET_BY_ID: {
id: "The id of the group to fetch"
id: "The ID of the group to fetch."
},
DELETE_USER: {
id: "The id of the group to remove the user from.",
id: "The ID of the group to remove the user from.",
username: "The username of the user to remove from the group."
}
} as const;
@ -118,7 +119,7 @@ export const AWS_AUTH = {
identityId: "The ID of the identity to login.",
iamHttpRequestMethod: "The HTTP request method used in the signed request.",
iamRequestUrl:
"The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/",
"The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/.",
iamRequestBody:
"The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.",
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request."
@ -129,8 +130,8 @@ export const AWS_AUTH = {
"The comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.",
allowedAccountIds:
"The comma-separated list of trusted AWS account IDs that are allowed to authenticate with Infisical.",
accessTokenTTL: "The lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
stsEndpoint: "The endpoint URL for the AWS STS API.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
@ -141,8 +142,8 @@ export const AWS_AUTH = {
"The new comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.",
allowedAccountIds:
"The new comma-separated list of trusted AWS account IDs that are allowed to authenticate with Infisical.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
stsEndpoint: "The new endpoint URL for the AWS STS API.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
@ -166,8 +167,8 @@ export const AZURE_AUTH = {
allowedServicePrincipalIds:
"The comma-separated list of Azure AD service principal IDs that are allowed to authenticate with Infisical.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
@ -177,8 +178,8 @@ export const AZURE_AUTH = {
allowedServicePrincipalIds:
"The new comma-separated list of Azure AD service principal IDs that are allowed to authenticate with Infisical.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
@ -202,8 +203,8 @@ export const GCP_AUTH = {
allowedZones:
"The comma-separated list of trusted zones that the GCE instances must belong to authenticate with Infisical.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
@ -215,8 +216,8 @@ export const GCP_AUTH = {
allowedZones:
"The new comma-separated list of trusted zones that the GCE instances must belong to authenticate with Infisical.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
@ -243,8 +244,8 @@ export const KUBERNETES_AUTH = {
allowedAudience:
"The optional audience claim that the service account JWT token must have to authenticate with Infisical.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
@ -275,15 +276,15 @@ export const TOKEN_AUTH = {
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
@ -295,18 +296,18 @@ export const TOKEN_AUTH = {
GET_TOKENS: {
identityId: "The ID of the identity to list token metadata for.",
offset: "The offset to start from. If you enter 10, it will start from the 10th token.",
limit: "The number of tokens to return"
limit: "The number of tokens to return."
},
CREATE_TOKEN: {
identityId: "The ID of the identity to create the token for.",
name: "The name of the token to create"
name: "The name of the token to create."
},
UPDATE_TOKEN: {
tokenId: "The ID of the token to update metadata for",
name: "The name of the token to update to"
tokenId: "The ID of the token to update metadata for.",
name: "The name of the token to update to."
},
REVOKE_TOKEN: {
tokenId: "The ID of the token to revoke"
tokenId: "The ID of the token to revoke."
}
} as const;
@ -323,8 +324,8 @@ export const OIDC_AUTH = {
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
boundSubject: "The expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
@ -336,8 +337,8 @@ export const OIDC_AUTH = {
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
boundSubject: "The new expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
@ -474,6 +475,7 @@ export const PROJECT_USERS = {
},
GET_USER_MEMBERSHIP: {
workspaceId: "The ID of the project to get memberships from.",
membershipId: "The ID of the user's project membership.",
username: "The username to get project membership of. Email is the default username."
},
UPDATE_USER_MEMBERSHIP: {
@ -505,8 +507,8 @@ export const PROJECT_IDENTITIES = {
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s,2m,3h",
temporaryAccessStartTime: "Time to which the temporary access starts"
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m ,3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
DELETE_IDENTITY_MEMBERSHIP: {
@ -523,8 +525,8 @@ export const PROJECT_IDENTITIES = {
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s,2m,3h",
temporaryAccessStartTime: "Time to which the temporary access starts"
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
}
};
@ -561,7 +563,7 @@ export const FOLDERS = {
directory: "The directory to list folders from. (Deprecated in favor of path)"
},
GET_BY_ID: {
folderId: "The id of the folder to get details."
folderId: "The ID of the folder to get details."
},
CREATE: {
workspaceId: "The ID of the project to create the folder in.",
@ -594,22 +596,22 @@ export const SECRETS = {
secretPath: "The path of the secret to attach tags to.",
type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located",
projectSlug: "The slug of the project where the secret is located",
projectSlug: "The slug of the project where the secret is located.",
tagSlugs: "An array of existing tag slugs to attach to the secret."
},
DETACH_TAGS: {
secretName: "The name of the secret to detach tags from.",
secretPath: "The path of the secret to detach tags from.",
type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located",
projectSlug: "The slug of the project where the secret is located",
environment: "The slug of the environment where the secret is located.",
projectSlug: "The slug of the project where the secret is located.",
tagSlugs: "An array of existing tag slugs to detach from the secret."
}
} as const;
export const RAW_SECRETS = {
LIST: {
expand: "Whether or not to expand secret references",
expand: "Whether or not to expand secret references.",
recursive:
"Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.",
workspaceId: "The ID of the project to list secrets from.",
@ -618,7 +620,7 @@ export const RAW_SECRETS = {
environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.",
includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets"
tagSlugs: "The comma separated tag slugs to filter secrets."
},
CREATE: {
secretName: "The name of the secret to create.",
@ -631,11 +633,11 @@ export const RAW_SECRETS = {
type: "The type of the secret to create.",
workspaceId: "The ID of the project to create the secret in.",
tagIds: "The ID of the tags to be attached to the created secret.",
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days",
secretReminderNote: "Note to be attached in notification email"
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.",
secretReminderNote: "Note to be attached in notification email."
},
GET: {
expand: "Whether or not to expand secret references",
expand: "Whether or not to expand secret references.",
secretName: "The name of the secret to get.",
workspaceId: "The ID of the project to get the secret from.",
workspaceSlug: "The slug of the project to get the secret from.",
@ -649,16 +651,16 @@ export const RAW_SECRETS = {
secretName: "The name of the secret to update.",
secretComment: "Update comment to the secret.",
environment: "The slug of the environment where the secret is located.",
secretPath: "The path of the secret to update",
secretPath: "The path of the secret to update.",
secretValue: "The new value of the secret.",
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to update.",
projectSlug: "The slug of the project to update the secret in.",
workspaceId: "The ID of the project to update the secret in.",
tagIds: "The ID of the tags to be attached to the updated secret.",
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days",
secretReminderNote: "Note to be attached in notification email",
newSecretName: "The new name for the secret"
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.",
secretReminderNote: "Note to be attached in notification email.",
newSecretName: "The new name for the secret."
},
DELETE: {
secretName: "The name of the secret to delete.",
@ -667,6 +669,12 @@ export const RAW_SECRETS = {
type: "The type of the secret to delete.",
projectSlug: "The slug of the project to delete the secret in.",
workspaceId: "The ID of the project where the secret is located."
},
GET_REFERENCE_TREE: {
secretName: "The name of the secret to get the reference tree for.",
workspaceId: "The ID of the project where the secret is located.",
environment: "The slug of the environment where the the secret is located.",
secretPath: "The folder path where the secret is located."
}
} as const;
@ -789,7 +797,7 @@ export const DYNAMIC_SECRETS = {
environmentSlug: "The slug of the environment to update the dynamic secret in.",
path: "The path to update the dynamic secret in.",
name: "The name of the dynamic secret.",
inputs: "The new partial values for the configurated provider of the dynamic secret",
inputs: "The new partial values for the configured provider of the dynamic secret",
defaultTTL: "The default TTL that will be applied for all the leases.",
maxTTL: "The maximum limit a TTL can be leases or renewed.",
newName: "The new name for the dynamic secret."
@ -800,7 +808,7 @@ export const DYNAMIC_SECRETS = {
path: "The path to delete the dynamic secret in.",
name: "The name of the dynamic secret.",
isForced:
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
"A boolean flag to delete the the dynamic secret from Infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
}
} as const;
@ -816,7 +824,7 @@ export const DYNAMIC_SECRET_LEASES = {
environmentSlug: "The slug of the environment of the dynamic secret in.",
path: "The path of the dynamic secret in.",
dynamicSecretName: "The name of the dynamic secret.",
ttl: "The lease lifetime ttl. If not provided the default TTL of dynamic secret will be used."
ttl: "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used."
},
RENEW: {
projectSlug: "The slug of the project of the dynamic secret in.",
@ -831,7 +839,7 @@ export const DYNAMIC_SECRET_LEASES = {
path: "The path of the dynamic secret in.",
leaseId: "The ID of the dynamic secret lease.",
isForced:
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
"A boolean flag to delete the the dynamic secret from Infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
}
} as const;
export const SECRET_TAGS = {
@ -840,11 +848,11 @@ export const SECRET_TAGS = {
},
GET_TAG_BY_ID: {
projectId: "The ID of the project to get tags from.",
tagId: "The ID of the tag to get details"
tagId: "The ID of the tag to get details."
},
GET_TAG_BY_SLUG: {
projectId: "The ID of the project to get tags from.",
tagSlug: "The slug of the tag to get details"
tagSlug: "The slug of the tag to get details."
},
CREATE: {
projectId: "The ID of the project to create the tag in.",
@ -854,7 +862,7 @@ export const SECRET_TAGS = {
},
UPDATE: {
projectId: "The ID of the project to update the tag in.",
tagId: "The ID of the tag to get details",
tagId: "The ID of the tag to get details.",
name: "The name of the tag to update.",
slug: "The slug of the tag to update.",
color: "The color of the tag to update."
@ -888,8 +896,8 @@ The permission object for the privilege.
privilegePermission: "The permission object for the privilege.",
isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
temporaryMode: "Type of temporary access given. Types: relative.",
temporaryRange: "TTL for the temporary time. Eg: 1m, 1h, 1d.",
temporaryAccessStartTime: "ISO time for which temporary access should begin."
},
UPDATE: {
@ -914,8 +922,8 @@ The permission object for the privilege.
`,
privilegePermission: "The permission object for the privilege.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
temporaryMode: "Type of temporary access given. Types: relative.",
temporaryRange: "TTL for the temporary time. Eg: 1m, 1h, 1d.",
temporaryAccessStartTime: "ISO time for which temporary access should begin."
},
DELETE: {
@ -931,62 +939,102 @@ The permission object for the privilege.
LIST: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.",
unpacked: "Whether the system should send the permissions as unpacked"
unpacked: "Whether the system should send the permissions as unpacked."
}
};
export const PROJECT_USER_ADDITIONAL_PRIVILEGE = {
CREATE: {
projectMembershipId: "Project membership id of user",
projectMembershipId: "Project membership ID of user.",
slug: "The slug of the privilege to create.",
permissions:
"The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape",
isPackPermission: "Whether the server should pack(compact) the permission object.",
"The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape.",
isPackPermission: "Whether the server should pack (compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
temporaryMode: "Type of temporary access given. Types: relative.",
temporaryRange: "TTL for the temporary time. Eg: 1m, 1h, 1d.",
temporaryAccessStartTime: "ISO time for which temporary access should begin."
},
UPDATE: {
privilegeId: "The id of privilege object",
privilegeId: "The ID of privilege object.",
slug: "The slug of the privilege to create.",
newSlug: "The new slug of the privilege to create.",
permissions:
"The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape",
isPackPermission: "Whether the server should pack(compact) the permission object.",
"The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape.",
isPackPermission: "Whether the server should pack (compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
temporaryMode: "Type of temporary access given. Types: relative.",
temporaryRange: "TTL for the temporary time. Eg: 1m, 1h, 1d.",
temporaryAccessStartTime: "ISO time for which temporary access should begin."
},
DELETE: {
privilegeId: "The id of privilege object"
privilegeId: "The ID of privilege object."
},
GET_BY_PRIVILEGEID: {
privilegeId: "The id of privilege object"
GET_BY_PRIVILEGE_ID: {
privilegeId: "The ID of privilege object."
},
LIST: {
projectMembershipId: "Project membership id of user"
projectMembershipId: "Project membership ID of user."
}
};
export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
CREATE: {
identityId: "The ID of the identity to create the privilege for.",
projectId: "The ID of the project of the identity in.",
slug: "The slug of the privilege to create.",
permission: "The permission for the privilege.",
isTemporary: "Whether the privilege is temporary or permanent.",
temporaryMode: "Type of temporary access given. Types: relative.",
temporaryRange: "The TTL for the temporary access given. Eg: 1m, 1h, 1d.",
temporaryAccessStartTime: "The start time in ISO format when the temporary access should begin."
},
UPDATE: {
id: "The ID of the identity privilege.",
identityId: "The ID of the identity to update.",
slug: "The slug of the privilege to update.",
privilegePermission: "The permission for the privilege.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative.",
temporaryRange: "The TTL for the temporary access given. Eg: 1m, 1h, 1d.",
temporaryAccessStartTime: "The start time in ISO format when the temporary access should begin."
},
DELETE: {
id: "The ID of the identity privilege.",
identityId: "The ID of the identity to delete.",
slug: "The slug of the privilege to delete."
},
GET_BY_SLUG: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.",
slug: "The slug of the privilege."
},
GET_BY_ID: {
id: "The ID of the identity privilege."
},
LIST: {
projectId: "The ID of the project that the identity is in.",
identityId: "The ID of the identity to list."
}
};
export const INTEGRATION_AUTH = {
GET: {
integrationAuthId: "The id of integration authentication object."
integrationAuthId: "The ID of integration authentication object."
},
DELETE: {
integration: "The slug of the integration to be unauthorized.",
projectId: "The ID of the project to delete the integration auth from."
},
DELETE_BY_ID: {
integrationAuthId: "The id of integration authentication object to delete."
integrationAuthId: "The ID of integration authentication object to delete."
},
CREATE_ACCESS_TOKEN: {
workspaceId: "The ID of the project to create the integration auth for.",
integration: "The slug of integration for the auth object.",
accessId: "The unique authorized access id of the external integration provider.",
accessId: "The unique authorized access ID of the external integration provider.",
accessToken: "The unique authorized access token of the external integration provider.",
awsAssumeIamRoleArn: "The AWS IAM Role to be assumed by Infisical",
awsAssumeIamRoleArn: "The AWS IAM Role to be assumed by Infisical.",
url: "",
namespace: "",
refreshToken: "The refresh token for integration authorization."
@ -1005,16 +1053,16 @@ export const INTEGRATION = {
targetEnvironment:
"The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
targetEnvironmentId:
"The target environment id of the integration provider. Used in cloudflare pages, teamcity, gitlab integrations.",
"The target environment ID of the integration provider. Used in cloudflare pages, teamcity, gitlab integrations.",
targetService:
"The service based grouping identifier of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
"The service based grouping identifier of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank.",
targetServiceId:
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank.",
owner: "External integration providers service entity owner. Used in Github.",
url: "The self-hosted URL of the platform to integrate with",
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault",
url: "The self-hosted URL of the platform to integrate with.",
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault.",
region: "AWS region to sync secrets to.",
scope: "Scope of the provider. Used by Github, Qovery",
scope: "Scope of the provider. Used by Github, Qovery.",
metadata: {
secretPrefix: "The prefix for the saved secret. Used by GCP.",
secretSuffix: "The suffix for the saved secret. Used by GCP.",
@ -1026,12 +1074,12 @@ export const INTEGRATION = {
githubVisibility:
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
githubVisibilityRepoIds:
"The repository IDs to sync secrets to when using the Github Integration. Only applicable when using Organization scope, and visibility is set to 'selected'",
"The repository IDs to sync secrets to when using the Github Integration. Only applicable when using Organization scope, and visibility is set to 'selected'.",
kmsKeyId: "The ID of the encryption key from AWS KMS.",
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
shouldEnableDelete: "The flag to enable deletion of secrets"
shouldEnableDelete: "The flag to enable deletion of secrets."
}
},
UPDATE: {
@ -1050,7 +1098,7 @@ export const INTEGRATION = {
integrationId: "The ID of the integration object."
},
SYNC: {
integrationId: "The ID of the integration object to manually sync"
integrationId: "The ID of the integration object to manually sync."
}
};
@ -1058,7 +1106,7 @@ export const AUDIT_LOG_STREAMS = {
CREATE: {
url: "The HTTP URL to push logs to.",
headers: {
desc: "The HTTP headers attached for the external prrovider requests.",
desc: "The HTTP headers attached for the external provider requests.",
key: "The HTTP header key name.",
value: "The HTTP header value."
}
@ -1067,7 +1115,7 @@ export const AUDIT_LOG_STREAMS = {
id: "The ID of the audit log stream to update.",
url: "The HTTP URL to push logs to.",
headers: {
desc: "The HTTP headers attached for the external prrovider requests.",
desc: "The HTTP headers attached for the external provider requests.",
key: "The HTTP header key name.",
value: "The HTTP header value."
}
@ -1083,16 +1131,16 @@ export const AUDIT_LOG_STREAMS = {
export const CERTIFICATE_AUTHORITIES = {
CREATE: {
projectSlug: "Slug of the project to create the CA in.",
type: "The type of CA to create",
friendlyName: "A friendly name for the CA",
organization: "The organization (O) for the CA",
ou: "The organization unit (OU) for the CA",
country: "The country name (C) for the CA",
province: "The state of province name for the CA",
locality: "The locality name for the CA",
commonName: "The common name (CN) for the CA",
notBefore: "The date and time when the CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
type: "The type of CA to create.",
friendlyName: "A friendly name for the CA.",
organization: "The organization (O) for the CA.",
ou: "The organization unit (OU) for the CA.",
country: "The country name (C) for the CA.",
province: "The state of province name for the CA.",
locality: "The locality name for the CA.",
commonName: "The common name (CN) for the CA.",
notBefore: "The date and time when the CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format.",
notAfter: "The date and time when the CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format.",
maxPathLength:
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
keyAlgorithm:
@ -1101,238 +1149,240 @@ export const CERTIFICATE_AUTHORITIES = {
"Whether or not certificates for this CA can only be issued through certificate templates."
},
GET: {
caId: "The ID of the CA to get"
caId: "The ID of the CA to get."
},
UPDATE: {
caId: "The ID of the CA to update",
status: "The status of the CA to update to. This can be one of active or disabled",
caId: "The ID of the CA to update.",
status: "The status of the CA to update to. This can be one of active or disabled.",
requireTemplateForIssuance:
"Whether or not certificates for this CA can only be issued through certificate templates."
},
DELETE: {
caId: "The ID of the CA to delete"
caId: "The ID of the CA to delete."
},
GET_CSR: {
caId: "The ID of the CA to generate CSR from",
csr: "The generated CSR from the CA"
caId: "The ID of the CA to generate CSR from.",
csr: "The generated CSR from the CA."
},
RENEW_CA_CERT: {
caId: "The ID of the CA to renew the CA certificate for",
caId: "The ID of the CA to renew the CA certificate for.",
type: "The type of behavior to use for the renewal operation. Currently Infisical is only able to renew a CA certificate with the same key pair.",
notAfter: "The expiry date and time for the renewed CA certificate in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The renewed CA certificate body",
certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the renewed CA certificate"
notAfter: "The expiry date and time for the renewed CA certificate in YYYY-MM-DDTHH:mm:ss.sssZ format.",
certificate: "The renewed CA certificate body.",
certificateChain: "The certificate chain of the CA.",
serialNumber: "The serial number of the renewed CA certificate."
},
GET_CERT: {
caId: "The ID of the CA to get the certificate body and certificate chain from",
certificate: "The certificate body of the CA",
certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the CA certificate"
caId: "The ID of the CA to get the certificate body and certificate chain from.",
certificate: "The certificate body of the CA.",
certificateChain: "The certificate chain of the CA.",
serialNumber: "The serial number of the CA certificate."
},
GET_CERT_BY_ID: {
caId: "The ID of the CA to get the CA certificate from",
caCertId: "The ID of the CA certificate to get"
caId: "The ID of the CA to get the CA certificate from.",
caCertId: "The ID of the CA certificate to get."
},
GET_CA_CERTS: {
caId: "The ID of the CA to get the CA certificates for",
certificate: "The certificate body of the CA certificate",
certificateChain: "The certificate chain of the CA certificate",
serialNumber: "The serial number of the CA certificate",
caId: "The ID of the CA to get the CA certificates for.",
certificate: "The certificate body of the CA certificate.",
certificateChain: "The certificate chain of the CA certificate.",
serialNumber: "The serial number of the CA certificate.",
version: "The version of the CA certificate. The version is incremented for each CA renewal operation."
},
SIGN_INTERMEDIATE: {
caId: "The ID of the CA to sign the intermediate certificate with",
csr: "The pem-encoded CSR to sign with the CA",
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
caId: "The ID of the CA to sign the intermediate certificate with.",
csr: "The pem-encoded CSR to sign with the CA.",
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format.",
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format.",
maxPathLength:
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
certificate: "The signed intermediate certificate",
certificateChain: "The certificate chain of the intermediate certificate",
issuingCaCertificate: "The certificate of the issuing CA",
serialNumber: "The serial number of the intermediate certificate"
certificate: "The signed intermediate certificate.",
certificateChain: "The certificate chain of the intermediate certificate.",
issuingCaCertificate: "The certificate of the issuing CA.",
serialNumber: "The serial number of the intermediate certificate."
},
IMPORT_CERT: {
caId: "The ID of the CA to import the certificate for",
certificate: "The certificate body to import",
certificateChain: "The certificate chain to import"
caId: "The ID of the CA to import the certificate for.",
certificate: "The certificate body to import.",
certificateChain: "The certificate chain to import."
},
ISSUE_CERT: {
caId: "The ID of the CA to issue the certificate from",
certificateTemplateId: "The ID of the certificate template to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
caId: "The ID of the CA to issue the certificate from.",
certificateTemplateId: "The ID of the certificate template to issue the certificate from.",
pkiCollectionId: "The ID of the PKI collection to add the certificate to.",
friendlyName: "A friendly name for the certificate.",
commonName: "The common name (CN) for the certificate.",
altNames:
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The issued certificate",
issuingCaCertificate: "The certificate of the issuing CA",
certificateChain: "The certificate chain of the issued certificate",
privateKey: "The private key of the issued certificate",
serialNumber: "The serial number of the issued certificate",
keyUsages: "The key usage extension of the certificate",
extendedKeyUsages: "The extended key usage extension of the certificate"
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format.",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format.",
certificate: "The issued certificate.",
issuingCaCertificate: "The certificate of the issuing CA.",
certificateChain: "The certificate chain of the issued certificate.",
privateKey: "The private key of the issued certificate.",
serialNumber: "The serial number of the issued certificate.",
keyUsages: "The key usage extension of the certificate.",
extendedKeyUsages: "The extended key usage extension of the certificate."
},
SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
keyUsages: "The key usage extension of the certificate",
extendedKeyUsages: "The extended key usage extension of the certificate",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
caId: "The ID of the CA to issue the certificate from.",
pkiCollectionId: "The ID of the PKI collection to add the certificate to.",
keyUsages: "The key usage extension of the certificate.",
extendedKeyUsages: "The extended key usage extension of the certificate.",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance.",
friendlyName: "A friendly name for the certificate.",
commonName: "The common name (CN) for the certificate.",
altNames:
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The issued certificate",
issuingCaCertificate: "The certificate of the issuing CA",
certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate"
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format.",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format.",
certificate: "The issued certificate.",
issuingCaCertificate: "The certificate of the issuing CA.",
certificateChain: "The certificate chain of the issued certificate.",
serialNumber: "The serial number of the issued certificate."
},
GET_CRLS: {
caId: "The ID of the CA to get the certificate revocation lists (CRLs) for",
id: "The ID of certificate revocation list (CRL)",
crl: "The certificate revocation list (CRL)"
caId: "The ID of the CA to get the certificate revocation lists (CRLs) for.",
id: "The ID of certificate revocation list (CRL).",
crl: "The certificate revocation list (CRL)."
}
};
export const CERTIFICATES = {
GET: {
serialNumber: "The serial number of the certificate to get"
serialNumber: "The serial number of the certificate to get."
},
REVOKE: {
serialNumber:
"The serial number of the certificate to revoke. The revoked certificate will be added to the certificate revocation list (CRL) of the CA.",
revocationReason: "The reason for revoking the certificate.",
revokedAt: "The date and time when the certificate was revoked",
revokedAt: "The date and time when the certificate was revoked.",
serialNumberRes: "The serial number of the revoked certificate."
},
DELETE: {
serialNumber: "The serial number of the certificate to delete"
serialNumber: "The serial number of the certificate to delete."
},
GET_CERT: {
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for",
certificate: "The certificate body of the certificate",
certificateChain: "The certificate chain of the certificate",
serialNumberRes: "The serial number of the certificate"
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
certificate: "The certificate body of the certificate.",
certificateChain: "The certificate chain of the certificate.",
serialNumberRes: "The serial number of the certificate."
}
};
export const CERTIFICATE_TEMPLATES = {
CREATE: {
caId: "The ID of the certificate authority to associate the template with",
pkiCollectionId: "The ID of the PKI collection to bind to the template",
name: "The name of the template",
commonName: "The regular expression string to use for validating common names",
subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
ttl: "The max TTL for the template",
keyUsages: "The key usage constraint or default value for when template is used during certificate issuance",
caId: "The ID of the certificate authority to associate the template with.",
pkiCollectionId: "The ID of the PKI collection to bind to the template.",
name: "The name of the template.",
commonName: "The regular expression string to use for validating common names.",
subjectAlternativeName: "The regular expression string to use for validating subject alternative names.",
ttl: "The max TTL for the template.",
keyUsages: "The key usage constraint or default value for when template is used during certificate issuance.",
extendedKeyUsages:
"The extended key usage constraint or default value for when template is used during certificate issuance"
"The extended key usage constraint or default value for when template is used during certificate issuance."
},
GET: {
certificateTemplateId: "The ID of the certificate template to get"
certificateTemplateId: "The ID of the certificate template to get."
},
UPDATE: {
certificateTemplateId: "The ID of the certificate template to update",
caId: "The ID of the certificate authority to update the association with the template",
pkiCollectionId: "The ID of the PKI collection to update the binding to the template",
name: "The updated name of the template",
commonName: "The updated regular expression string for validating common names",
subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
ttl: "The updated max TTL for the template",
certificateTemplateId: "The ID of the certificate template to update.",
caId: "The ID of the certificate authority to update the association with the template.",
pkiCollectionId: "The ID of the PKI collection to update the binding to the template.",
name: "The updated name of the template.",
commonName: "The updated regular expression string for validating common names.",
subjectAlternativeName: "The updated regular expression string for validating subject alternative names.",
ttl: "The updated max TTL for the template.",
keyUsages:
"The updated key usage constraint or default value for when template is used during certificate issuance",
"The updated key usage constraint or default value for when template is used during certificate issuance.",
extendedKeyUsages:
"The updated extended key usage constraint or default value for when template is used during certificate issuance"
"The updated extended key usage constraint or default value for when template is used during certificate issuance."
},
DELETE: {
certificateTemplateId: "The ID of the certificate template to delete"
certificateTemplateId: "The ID of the certificate template to delete."
}
};
export const CA_CRLS = {
GET: {
crlId: "The ID of the certificate revocation list (CRL) to get",
crl: "The certificate revocation list (CRL)"
crlId: "The ID of the certificate revocation list (CRL) to get.",
crl: "The certificate revocation list (CRL)."
}
};
export const ALERTS = {
CREATE: {
projectId: "The ID of the project to create the alert in",
pkiCollectionId: "The ID of the PKI collection to bind to the alert",
name: "The name of the alert",
alertBeforeDays: "The number of days before the certificate expires to trigger the alert",
emails: "The email addresses to send the alert email to"
projectId: "The ID of the project to create the alert in.",
pkiCollectionId: "The ID of the PKI collection to bind to the alert.",
name: "The name of the alert.",
alertBeforeDays: "The number of days before the certificate expires to trigger the alert.",
emails: "The email addresses to send the alert email to."
},
GET: {
alertId: "The ID of the alert to get"
alertId: "The ID of the alert to get."
},
UPDATE: {
alertId: "The ID of the alert to update",
name: "The name of the alert to update to",
alertBeforeDays: "The number of days before the certificate expires to trigger the alert to update to",
pkiCollectionId: "The ID of the PKI collection to bind to the alert to update to",
emails: "The email addresses to send the alert email to update to"
alertId: "The ID of the alert to update.",
name: "The name of the alert to update to.",
alertBeforeDays: "The number of days before the certificate expires to trigger the alert to update to.",
pkiCollectionId: "The ID of the PKI collection to bind to the alert to update to.",
emails: "The email addresses to send the alert email to update to."
},
DELETE: {
alertId: "The ID of the alert to delete"
alertId: "The ID of the alert to delete."
}
};
export const PKI_COLLECTIONS = {
CREATE: {
projectId: "The ID of the project to create the PKI collection in",
name: "The name of the PKI collection",
description: "A description for the PKI collection"
projectId: "The ID of the project to create the PKI collection in.",
name: "The name of the PKI collection.",
description: "A description for the PKI collection."
},
GET: {
collectionId: "The ID of the PKI collection to get"
collectionId: "The ID of the PKI collection to get."
},
UPDATE: {
collectionId: "The ID of the PKI collection to update",
name: "The name of the PKI collection to update to",
description: "The description for the PKI collection to update to"
collectionId: "The ID of the PKI collection to update.",
name: "The name of the PKI collection to update to.",
description: "The description for the PKI collection to update to."
},
DELETE: {
collectionId: "The ID of the PKI collection to delete"
collectionId: "The ID of the PKI collection to delete."
},
LIST_ITEMS: {
collectionId: "The ID of the PKI collection to list items from",
type: "The type of the PKI collection item to list",
offset: "The offset to start from",
limit: "The number of items to return"
collectionId: "The ID of the PKI collection to list items from.",
type: "The type of the PKI collection item to list.",
offset: "The offset to start from.",
limit: "The number of items to return."
},
ADD_ITEM: {
collectionId: "The ID of the PKI collection to add the item to",
type: "The type of the PKI collection item to add",
itemId: "The resource ID of the PKI collection item to add"
collectionId: "The ID of the PKI collection to add the item to.",
type: "The type of the PKI collection item to add.",
itemId: "The resource ID of the PKI collection item to add."
},
DELETE_ITEM: {
collectionId: "The ID of the PKI collection to delete the item from",
collectionItemId: "The ID of the PKI collection item to delete",
type: "The type of the deleted PKI collection item",
itemId: "The resource ID of the deleted PKI collection item"
collectionId: "The ID of the PKI collection to delete the item from.",
collectionItemId: "The ID of the PKI collection item to delete.",
type: "The type of the deleted PKI collection item.",
itemId: "The resource ID of the deleted PKI collection item."
}
};
export const PROJECT_ROLE = {
CREATE: {
projectSlug: "Slug of the project to create the role for.",
projectId: "Id of the project to create the role for.",
slug: "The slug of the role.",
name: "The name of the role.",
description: "The description for the role.",
permissions: "The permissions assigned to the role."
},
UPDATE: {
projectSlug: "Slug of the project to update the role for.",
projectSlug: "The slug of the project to update the role for.",
projectId: "The ID of the project to update the role for.",
roleId: "The ID of the role to update",
slug: "The slug of the role.",
name: "The name of the role.",
@ -1340,15 +1390,18 @@ export const PROJECT_ROLE = {
permissions: "The permissions assigned to the role."
},
DELETE: {
projectSlug: "Slug of the project to delete this role for.",
projectSlug: "The slug of the project to delete this role for.",
projectId: "The ID of the project to delete the role for.",
roleId: "The ID of the role to update"
},
GET_ROLE_BY_SLUG: {
projectSlug: "The slug of the project.",
roleSlug: "The slug of the role to get details"
projectId: "The ID of the project.",
roleSlug: "The slug of the role to get details."
},
LIST: {
projectSlug: "The slug of the project to list the roles of."
projectSlug: "The slug of the project to list the roles of.",
projectId: "The ID of the project."
}
};

View File

@ -1,111 +0,0 @@
import { AnyAbility, ExtractSubjectType } from "@casl/ability";
import { AbilityQuery, rulesToQuery } from "@casl/ability/extra";
import { Tables } from "knex/types/tables";
import { BadRequestError, UnauthorizedError } from "../errors";
import { TKnexDynamicOperator } from "../knex/dynamic";
type TBuildKnexQueryFromCaslDTO<K extends AnyAbility> = {
ability: K;
subject: ExtractSubjectType<Parameters<K["rulesFor"]>[1]>;
action: Parameters<K["rulesFor"]>[0];
};
export const buildKnexQueryFromCaslOperators = <K extends AnyAbility>({
ability,
subject,
action
}: TBuildKnexQueryFromCaslDTO<K>) => {
const query = rulesToQuery(ability, action, subject, (rule) => {
if (!rule.ast) throw new Error("Ast not defined");
return rule.ast;
});
if (query === null) throw new UnauthorizedError({ message: `You don't have permission to do ${action} ${subject}` });
return query;
};
type TFieldMapper<T extends keyof Tables> = {
[K in T]: `${K}.${Exclude<keyof Tables[K]["base"], symbol>}`;
}[T];
type TFormatCaslFieldsWithTableNames<T extends keyof Tables> = {
// handle if any missing operator else throw error let the app break because this is executing again the db
missingOperatorCallback?: (operator: string) => void;
fieldMapping: (arg: string) => TFieldMapper<T> | null;
dynamicQuery: TKnexDynamicOperator;
};
export const formatCaslOperatorFieldsWithTableNames = <T extends keyof Tables>({
missingOperatorCallback = (arg) => {
throw new BadRequestError({ message: `Unknown permission operator: ${arg}` });
},
dynamicQuery: dynamicQueryAst,
fieldMapping
}: TFormatCaslFieldsWithTableNames<T>) => {
const stack: [TKnexDynamicOperator, TKnexDynamicOperator | null][] = [[dynamicQueryAst, null]];
while (stack.length) {
const [filterAst, parentAst] = stack.pop()!;
if (filterAst.operator === "and" || filterAst.operator === "or" || filterAst.operator === "not") {
filterAst.value.forEach((el) => {
stack.push([el, filterAst]);
});
// eslint-disable-next-line no-continue
continue;
}
if (
filterAst.operator === "eq" ||
filterAst.operator === "ne" ||
filterAst.operator === "in" ||
filterAst.operator === "endsWith" ||
filterAst.operator === "startsWith"
) {
const attrPath = fieldMapping(filterAst.field);
if (attrPath) {
filterAst.field = attrPath;
} else if (parentAst && Array.isArray(parentAst.value)) {
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
} else throw new Error("Unknown casl field");
// eslint-disable-next-line no-continue
continue;
}
if (parentAst && Array.isArray(parentAst.value)) {
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
} else {
missingOperatorCallback?.(filterAst.operator);
}
}
return dynamicQueryAst;
};
export const convertCaslOperatorToKnexOperator = <T extends keyof Tables>(
caslKnexOperators: AbilityQuery,
fieldMapping: (arg: string) => TFieldMapper<T> | null
) => {
const value = [];
if (caslKnexOperators.$and) {
value.push({
operator: "not" as const,
value: caslKnexOperators.$and as TKnexDynamicOperator[]
});
}
if (caslKnexOperators.$or) {
value.push({
operator: "or" as const,
value: caslKnexOperators.$or as TKnexDynamicOperator[]
});
}
return formatCaslOperatorFieldsWithTableNames({
dynamicQuery: {
operator: "and",
value
},
fieldMapping
});
};

View File

@ -142,6 +142,7 @@ const envSchema = z
SECRET_SCANNING_WEBHOOK_SECRET: zpStr(z.string().optional()),
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()),
// LICENSE
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
@ -177,7 +178,8 @@ const envSchema = z
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
}));
let envCfg: Readonly<z.infer<typeof envSchema>>;

View File

@ -71,6 +71,13 @@ export class BadRequestError extends Error {
}
}
export class RateLimitError extends Error {
constructor({ message }: { message?: string }) {
super(message || "Rate limit exceeded");
this.name = "RateLimitExceeded";
}
}
export class NotFoundError extends Error {
name: string;

View File

@ -81,3 +81,25 @@ export const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
}
return chunks;
};
/*
* Returns all items from the first list that
* do not exist in the second list.
*/
export const diff = <T>(
root: readonly T[],
other: readonly T[],
identity: (item: T) => string | number | symbol = (t: T) => t as unknown as string | number | symbol
): T[] => {
if (!root?.length && !other?.length) return [];
if (root?.length === undefined) return [...other];
if (!other?.length) return [...root];
const bKeys = other.reduce(
(acc, item) => {
acc[identity(item)] = true;
return acc;
},
{} as Record<string | number | symbol, boolean>
);
return root.filter((a) => !bKeys[identity(a)]);
};

View File

@ -2,32 +2,31 @@ import { Knex } from "knex";
import { UnauthorizedError } from "../errors";
type TKnexDynamicPrimitiveOperator = {
type TKnexDynamicPrimitiveOperator<T extends object> = {
operator: "eq" | "ne" | "startsWith" | "endsWith";
value: string;
field: string;
field: Extract<keyof T, string>;
};
type TKnexDynamicInOperator = {
type TKnexDynamicInOperator<T extends object> = {
operator: "in";
value: string[] | number[];
field: string;
field: Extract<keyof T, string>;
};
type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator;
type TKnexNonGroupOperator<T extends object> = TKnexDynamicInOperator<T> | TKnexDynamicPrimitiveOperator<T>;
type TKnexGroupOperator = {
type TKnexGroupOperator<T extends object> = {
operator: "and" | "or" | "not";
value: (TKnexNonGroupOperator | TKnexGroupOperator)[];
value: (TKnexNonGroupOperator<T> | TKnexGroupOperator<T>)[];
};
// akhilmhdh: This is still in pending state and not yet ready. If you want to use it ping me.
// used when you need to write a complex query with the orm
// use it when you need complex or and and condition - most of the time not needed
// majorly used with casl permission to filter data based on permission
export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator;
export type TKnexDynamicOperator<T extends object> = TKnexGroupOperator<T> | TKnexNonGroupOperator<T>;
export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => {
export const buildDynamicKnexQuery = <T extends object>(
rootQueryBuild: Knex.QueryBuilder,
dynamicQuery: TKnexDynamicOperator<T>
) => {
const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }];
while (stack.length) {
@ -50,34 +49,25 @@ export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQu
break;
}
case "and": {
void queryBuilder.andWhere((subQueryBuilder) => {
filterAst.value.forEach((el) => {
stack.push({
queryBuilder: subQueryBuilder,
filterAst: el
});
filterAst.value.forEach((el) => {
void queryBuilder.andWhere((subQueryBuilder) => {
buildDynamicKnexQuery(subQueryBuilder, el);
});
});
break;
}
case "or": {
void queryBuilder.orWhere((subQueryBuilder) => {
filterAst.value.forEach((el) => {
stack.push({
queryBuilder: subQueryBuilder,
filterAst: el
});
filterAst.value.forEach((el) => {
void queryBuilder.orWhere((subQueryBuilder) => {
buildDynamicKnexQuery(subQueryBuilder, el);
});
});
break;
}
case "not": {
void queryBuilder.whereNot((subQueryBuilder) => {
filterAst.value.forEach((el) => {
stack.push({
queryBuilder: subQueryBuilder,
filterAst: el
});
filterAst.value.forEach((el) => {
void queryBuilder.whereNot((subQueryBuilder) => {
buildDynamicKnexQuery(subQueryBuilder, el);
});
});
break;

View File

@ -3,6 +3,7 @@ import { Knex } from "knex";
import { Tables } from "knex/types/tables";
import { DatabaseError } from "../errors";
import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
export * from "./connection";
export * from "./join";
@ -20,9 +21,10 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
export type TFindFilter<R extends object = object> = Partial<R> & {
$in?: Partial<{ [k in keyof R]: R[k][] }>;
$search?: Partial<{ [k in keyof R]: R[k] }>;
$complex?: TKnexDynamicOperator<R>;
};
export const buildFindFilter =
<R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) =>
<R extends object = object>({ $in, $search, $complex, ...filter }: TFindFilter<R>) =>
(bd: Knex.QueryBuilder<R, R>) => {
void bd.where(filter);
if ($in) {
@ -39,6 +41,9 @@ export const buildFindFilter =
}
});
}
if ($complex) {
return buildDynamicKnexQuery(bd, $complex);
}
return bd;
};

View File

@ -2,6 +2,7 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-lim
import { Redis } from "ioredis";
import { getConfig } from "@app/lib/config/env";
import { RateLimitError } from "@app/lib/errors";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
const appCfg = getConfig();
@ -10,6 +11,11 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
: null;
return {
errorResponseBuilder: (_, context) => {
throw new RateLimitError({
message: `Rate limit exceeded. Please try again in ${context.after}`
});
},
timeWindow: 60 * 1000,
max: 600,
redis,

View File

@ -18,6 +18,7 @@ export type TAuthMode =
user: TUsers;
orgId: string;
authMethod: AuthMethod;
isMfaVerified?: boolean;
}
| {
authMode: AuthMode.API_KEY;
@ -121,7 +122,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
tokenVersionId,
actor,
orgId: orgId as string,
authMethod: token.authMethod
authMethod: token.authMethod,
isMfaVerified: token.isMfaVerified
};
break;
}

View File

@ -10,6 +10,7 @@ import {
GatewayTimeoutError,
InternalServerError,
NotFoundError,
RateLimitError,
ScimRequestError,
UnauthorizedError
} from "@app/lib/errors";
@ -27,7 +28,8 @@ enum HttpStatusCodes {
Forbidden = 403,
// eslint-disable-next-line @typescript-eslint/no-shadow
InternalServerError = 500,
GatewayTimeout = 504
GatewayTimeout = 504,
TooManyRequests = 429
}
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
@ -61,7 +63,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
void res.status(HttpStatusCodes.Forbidden).send({
statusCode: HttpStatusCodes.Forbidden,
error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType}`
message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}`
});
} else if (error instanceof ForbiddenRequestError) {
void res.status(HttpStatusCodes.Forbidden).send({
@ -69,6 +71,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
message: error.message,
error: error.name
});
} else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({
statusCode: HttpStatusCodes.TooManyRequests,
message: error.message,
error: error.name
});
} else if (error instanceof ScimRequestError) {
void res.status(error.status).send({
schemas: error.schemas,

View File

@ -5,6 +5,7 @@ import { z } from "zod";
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
import { registerV1EERoutes } from "@app/ee/routes/v1";
import { registerV2EERoutes } from "@app/ee/routes/v2";
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
@ -32,6 +33,7 @@ import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
@ -225,9 +227,7 @@ export const registerRoutes = async (
}: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
) => {
const appCfg = getConfig();
if (!appCfg.DISABLE_SECRET_SCANNING) {
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
}
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
// db layers
const userDAL = userDALFactory(db);
@ -1077,6 +1077,14 @@ export const registerRoutes = async (
permissionService,
identityProjectDAL
});
const identityProjectAdditionalPrivilegeV2Service = identityProjectAdditionalPrivilegeV2ServiceFactory({
projectDAL,
identityProjectAdditionalPrivilegeDAL,
permissionService,
identityProjectDAL
});
const identityTokenAuthService = identityTokenAuthServiceFactory({
identityTokenAuthDAL,
identityDAL,
@ -1326,6 +1334,7 @@ export const registerRoutes = async (
telemetry: telemetryService,
projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
identityProjectAdditionalPrivilegeV2: identityProjectAdditionalPrivilegeV2Service,
secretSharing: secretSharingService,
userEngagement: userEngagementService,
externalKms: externalKmsService,
@ -1424,7 +1433,13 @@ export const registerRoutes = async (
},
{ prefix: "/api/v1" }
);
await server.register(registerV2Routes, { prefix: "/api/v2" });
await server.register(
async (v2Server) => {
await v2Server.register(registerV2EERoutes);
await v2Server.register(registerV2Routes);
},
{ prefix: "/api/v2" }
);
await server.register(registerV3Routes, { prefix: "/api/v3" });
server.addHook("onClose", async () => {

View File

@ -9,9 +9,10 @@ import {
SecretApprovalPoliciesSchema,
UsersSchema
} from "@app/db/schemas";
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { UnpackedPermissionSchema } from "./santizedSchemas/permission";
// sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod
export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
@ -149,13 +150,44 @@ export const ProjectSpecificPrivilegePermissionSchema = z.object({
});
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
permissions: UnpackedPermissionSchema.array()
permissions: UnpackedPermissionSchema.array().transform((permissions) =>
permissions.filter(
(caslRule) =>
![
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretFolders
].includes((caslRule?.subject as ProjectPermissionSub) || "")
)
)
});
export const SanitizedRoleSchema = ProjectRolesSchema.extend({
permissions: UnpackedPermissionSchema.array()
});
export const SanitizedRoleSchemaV1 = ProjectRolesSchema.extend({
permissions: UnpackedPermissionSchema.array().transform((caslPermission) =>
// first map and remove other actions of folder permission
caslPermission
.map((caslRule) =>
caslRule.subject === ProjectPermissionSub.SecretFolders
? {
...caslRule,
action: caslRule.action.filter((caslAction) => caslAction === ProjectPermissionActions.Read)
}
: caslRule
)
// now filter out dynamic secret, secret import permission
.filter(
(caslRule) =>
![ProjectPermissionSub.DynamicSecrets, ProjectPermissionSub.SecretImports].includes(
(caslRule?.subject as ProjectPermissionSub) || ""
) && caslRule.action.length > 0
)
)
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true,
inputTag: true,

View File

@ -0,0 +1,7 @@
import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas";
import { UnpackedPermissionSchema } from "./permission";
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
permissions: UnpackedPermissionSchema.array()
});

View File

@ -0,0 +1,16 @@
import { MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import { z } from "zod";
export const UnpackedPermissionSchema = z.object({
subject: z
.union([z.string().min(1), z.string().array()])
.transform((el) => (typeof el !== "string" ? el[0] : el))
.optional(),
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
conditions: z.unknown().optional(),
inverted: z.boolean().optional()
});
export const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility>>[]));

View File

@ -0,0 +1,7 @@
import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas";
import { UnpackedPermissionSchema } from "./permission";
export const SanitizedUserProjectAdditionalPrivilegeSchema = ProjectUserAdditionalPrivilegeSchema.extend({
permissions: UnpackedPermissionSchema.array()
});

View File

@ -107,7 +107,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
userId: decodedToken.userId,
tokenVersionId: tokenVersion.id,
accessVersion: tokenVersion.accessVersion,
organizationId: decodedToken.organizationId
organizationId: decodedToken.organizationId,
isMfaVerified: decodedToken.isMfaVerified
},
appCfg.AUTH_SECRET,
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }

View File

@ -293,10 +293,10 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
schema: {
description: "Decrypt data with KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
keyId: z.string().uuid().describe(KMS.DECRYPT.keyId)
}),
body: z.object({
ciphertext: base64Schema.describe(KMS.ENCRYPT.plaintext)
ciphertext: base64Schema.describe(KMS.DECRYPT.ciphertext)
}),
response: {
200: z.object({

View File

@ -3,7 +3,10 @@ import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { DASHBOARD } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
@ -192,15 +195,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
req.permission.orgId
);
const permissiveEnvs = // filter envs user has access to
const allowedDynamicSecretEnvironments = // filter envs user has access to
environments.filter((environment) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
)
);
if (includeDynamicSecrets && permissiveEnvs.length) {
if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) {
// this is the unique count, ie duplicate secrets across envs only count as 1
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
actor: req.permission.type,
@ -209,7 +212,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
projectId,
search,
environmentSlugs: permissiveEnvs,
environmentSlugs: allowedDynamicSecretEnvironments,
path: secretPath,
isInternal: true
});
@ -224,7 +227,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search,
orderBy,
orderDirection,
environmentSlugs: permissiveEnvs,
environmentSlugs: allowedDynamicSecretEnvironments,
path: secretPath,
limit: remainingLimit,
offset: adjustedOffset,
@ -241,13 +244,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
if (includeSecrets && permissiveEnvs.length) {
if (includeSecrets) {
// this is the unique count, ie duplicate secrets across envs only count as 1
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments: permissiveEnvs,
environments,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
@ -260,7 +263,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments: permissiveEnvs,
environments,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
@ -272,7 +275,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
isInternal: true
});
for await (const environment of permissiveEnvs) {
for await (const environment of environments) {
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountFromEnv) {

View File

@ -258,7 +258,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.refine((v) => slugify(v) === v, {
message: "Membership role must be a valid slug"
})
.optional()
.optional(),
enforceMfa: z.boolean().optional()
}),
response: {
200: z.object({

View File

@ -39,7 +39,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
email: true,
firstName: true,
lastName: true,
id: true
id: true,
username: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
roles: z.array(
z.object({
@ -56,7 +57,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
})
)
})
.omit({ createdAt: true, updatedAt: true })
.omit({ updatedAt: true })
.array()
})
}
@ -74,6 +75,65 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
}
});
server.route({
method: "GET",
url: "/:workspaceId/memberships/:membershipId",
config: {
rateLimit: readLimit
},
schema: {
description: "Return project user membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId),
membershipId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.membershipId)
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema.extend({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true,
username: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
)
}).omit({ updatedAt: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const membership = await server.services.projectMembership.getProjectMembershipById({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
id: req.params.membershipId
});
return { membership };
}
});
server.route({
method: "POST",
url: "/:workspaceId/memberships/details",

View File

@ -280,10 +280,6 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
providerAuthToken: req.body.providerAuthToken
});
if (data.isMfaEnabled) {
return { mfaEnabled: true, token: data.token } as const; // for discriminated union
}
void res.setCookie("jid", data.token.refresh, {
httpOnly: true,
path: "/",
@ -292,7 +288,6 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
});
return {
mfaEnabled: false,
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,
publicKey: data.user.publicKey,

View File

@ -47,7 +47,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
token: z.string()
token: z.string(),
isMfaEnabled: z.boolean()
})
}
},
@ -60,6 +61,13 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
ipAddress: req.realIp
});
if (tokens.isMfaEnabled) {
return {
token: tokens.mfa as string,
isMfaEnabled: true
};
}
void res.setCookie("jid", tokens.refresh, {
httpOnly: true,
path: "/",
@ -67,7 +75,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED
});
return { token: tokens.access };
return { token: tokens.access, isMfaEnabled: false };
}
});
@ -86,21 +94,17 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
password: z.string().optional()
}),
response: {
200: z.discriminatedUnion("mfaEnabled", [
z.object({ mfaEnabled: z.literal(true), token: z.string() }),
z.object({
mfaEnabled: z.literal(false),
encryptionVersion: z.number().default(1).nullable().optional(),
protectedKey: z.string().nullable(),
protectedKeyIV: z.string().nullable(),
protectedKeyTag: z.string().nullable(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
token: z.string()
})
])
200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(),
protectedKey: z.string().nullable(),
protectedKeyIV: z.string().nullable(),
protectedKeyTag: z.string().nullable(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
token: z.string()
})
}
},
handler: async (req, res) => {
@ -118,10 +122,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
password: req.body.password
});
if (data.isMfaEnabled) {
return { mfaEnabled: true, token: data.token } as const; // for discriminated union
}
void res.setCookie("jid", data.token.refresh, {
httpOnly: true,
path: "/",
@ -130,7 +130,6 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
});
return {
mfaEnabled: false,
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,
publicKey: data.user.publicKey,

View File

@ -23,6 +23,18 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas";
const SecretReferenceNode = z.object({
key: z.string(),
value: z.string().optional(),
environment: z.string(),
secretPath: z.string()
});
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
children: z.lazy(() => SecretReferenceNodeTree.array())
});
export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@ -2102,6 +2114,58 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/raw/:secretName/secret-reference-tree",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Get secret reference tree",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName)
}),
querystring: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.workspaceId),
environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath)
}),
response: {
200: z.object({
tree: SecretReferenceNodeTree,
value: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretName } = req.params;
const { secretPath, environment, workspaceId } = req.query;
const { tree, value } = await server.services.secret.getSecretReferenceTree({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: workspaceId,
secretName,
secretPath,
environment
});
return { tree, value };
}
});
server.route({
method: "POST",
url: "/backfill-secret-references",

View File

@ -99,13 +99,15 @@ export const authLoginServiceFactory = ({
ip,
userAgent,
organizationId,
authMethod
authMethod,
isMfaVerified
}: {
user: TUsers;
ip: string;
userAgent: string;
organizationId?: string;
authMethod: AuthMethod;
isMfaVerified?: boolean;
}) => {
const cfg = getConfig();
await updateUserDeviceSession(user, ip, userAgent);
@ -123,7 +125,8 @@ export const authLoginServiceFactory = ({
userId: user.id,
tokenVersionId: tokenSession.id,
accessVersion: tokenSession.accessVersion,
organizationId
organizationId,
isMfaVerified
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
@ -136,7 +139,8 @@ export const authLoginServiceFactory = ({
userId: user.id,
tokenVersionId: tokenSession.id,
refreshVersion: tokenSession.refreshVersion,
organizationId
organizationId,
isMfaVerified
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
@ -298,30 +302,6 @@ export const authLoginServiceFactory = ({
});
}
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) {
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
const mfaToken = jwt.sign(
{
authMethod,
authTokenType: AuthTokenType.MFA_TOKEN,
userId: userEnc.userId
},
cfg.AUTH_SECRET,
{
expiresIn: cfg.JWT_MFA_LIFETIME
}
);
await sendUserMfaCode({
userId: userEnc.userId,
email: userEnc.email
});
return { isMfaEnabled: true, token: mfaToken } as const;
}
const token = await generateUserTokens({
user: {
...userEnc,
@ -333,7 +313,7 @@ export const authLoginServiceFactory = ({
organizationId
});
return { token, isMfaEnabled: false, user: userEnc } as const;
return { token, user: userEnc } as const;
};
const selectOrganization = async ({
@ -373,15 +353,43 @@ export const authLoginServiceFactory = ({
});
}
// send multi factor auth token if they it enabled
if ((selectedOrg.enforceMfa || user.isMfaEnabled) && user.email && !decodedToken.isMfaVerified) {
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
const mfaToken = jwt.sign(
{
authMethod: decodedToken.authMethod,
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user.id
},
cfg.AUTH_SECRET,
{
expiresIn: cfg.JWT_MFA_LIFETIME
}
);
await sendUserMfaCode({
userId: user.id,
email: user.email
});
return { isMfaEnabled: true, mfa: mfaToken } as const;
}
const tokens = await generateUserTokens({
authMethod: decodedToken.authMethod,
user,
userAgent,
ip: ipAddress,
organizationId
organizationId,
isMfaVerified: decodedToken.isMfaVerified
});
return tokens;
return {
...tokens,
isMfaEnabled: false
};
};
/*
@ -504,7 +512,8 @@ export const authLoginServiceFactory = ({
ip,
userAgent,
organizationId: orgId,
authMethod: decodedToken.authMethod
authMethod: decodedToken.authMethod,
isMfaVerified: true
});
return { token, user: userEnc };
@ -629,7 +638,6 @@ export const authLoginServiceFactory = ({
const oauth2TokenExchange = async ({ userAgent, ip, providerAuthToken, email }: TOauthTokenExchangeDTO) => {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
const appCfg = getConfig();
const { authMethod, userName } = decodedProviderToken;
if (!userName) throw new BadRequestError({ message: "Missing user name" });
const organizationId =
@ -644,29 +652,6 @@ export const authLoginServiceFactory = ({
if (!userEnc) throw new BadRequestError({ message: "Invalid token" });
if (!userEnc.serverEncryptedPrivateKey)
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) {
enforceUserLockStatus(Boolean(userEnc.isLocked), userEnc.temporaryLockDateEnd);
const mfaToken = jwt.sign(
{
authMethod,
authTokenType: AuthTokenType.MFA_TOKEN,
userId: userEnc.userId
},
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_MFA_LIFETIME
}
);
await sendUserMfaCode({
userId: userEnc.userId,
email: userEnc.email
});
return { isMfaEnabled: true, token: mfaToken } as const;
}
const token = await generateUserTokens({
user: { ...userEnc, id: userEnc.userId },

View File

@ -52,6 +52,7 @@ export type AuthModeJwtTokenPayload = {
tokenVersionId: string;
accessVersion: number;
organizationId?: string;
isMfaVerified?: boolean;
};
export type AuthModeMfaJwtTokenPayload = {
@ -69,6 +70,7 @@ export type AuthModeRefreshJwtTokenPayload = {
tokenVersionId: string;
refreshVersion: number;
organizationId?: string;
isMfaVerified?: boolean;
};
export type AuthModeProviderJwtTokenPayload = {

View File

@ -4,7 +4,7 @@ import sjcl from "sjcl";
import tweetnacl from "tweetnacl";
import tweetnaclUtil from "tweetnacl-util";
import { SecretType } from "@app/db/schemas";
import { SecretType, TSecretFolders } from "@app/db/schemas";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { chunkArray } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
@ -19,7 +19,7 @@ import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { fnSecretBulkInsert, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
@ -35,7 +35,7 @@ export type TImportDataIntoInfisicalDTO = {
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findById">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
@ -67,6 +67,7 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
const infisicalImportData: InfisicalImportData = {
projects: [],
environments: [],
folders: [],
secrets: []
};
@ -80,25 +81,387 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
envTemplates.set(env.id, env.defaultName);
}
// environments
for (const env of parsedJson.baseEnvironments) {
infisicalImportData.environments.push({
id: env.id,
name: envTemplates.get(env.environmentRoleId)!,
projectId: env.envParentId
});
// custom base environments
for (const env of parsedJson.nonDefaultEnvironmentRoles) {
envTemplates.set(env.id, env.name);
}
// secrets
// environments
for (const env of parsedJson.baseEnvironments) {
const appId = parsedJson.apps.find((a) => a.id === env.envParentId)?.id;
// If we find the app from the envParentId, we know this is a root-level environment.
if (appId) {
infisicalImportData.environments.push({
id: env.id,
name: envTemplates.get(env.environmentRoleId)!,
projectId: appId
});
}
}
const findRootInheritedSecret = (
secret: { val?: string; inheritsEnvironmentId?: string },
secretName: string,
envs: typeof parsedJson.envs
): { val?: string } => {
if (!secret) {
return {
val: ""
};
}
// If we have a direct value, return it
if (secret.val !== undefined) {
return secret;
}
// If there's no inheritance, return the secret as is
if (!secret.inheritsEnvironmentId) {
return secret;
}
const inheritedEnv = envs[secret.inheritsEnvironmentId];
if (!inheritedEnv) return secret;
return findRootInheritedSecret(inheritedEnv.variables[secretName], secretName, envs);
};
const processBranches = () => {
for (const subEnv of parsedJson.subEnvironments) {
const app = parsedJson.apps.find((a) => a.id === subEnv.envParentId);
const block = parsedJson.blocks.find((b) => b.id === subEnv.envParentId);
if (app) {
// Handle regular app branches
const branchEnvironment = infisicalImportData.environments.find((e) => e.id === subEnv.parentEnvironmentId);
infisicalImportData.folders.push({
name: subEnv.subName,
parentFolderId: subEnv.parentEnvironmentId,
environmentId: branchEnvironment!.id,
id: subEnv.id
});
}
if (block) {
// Handle block branches
// 1. Find all apps that use this block
const appsUsingBlock = parsedJson.appBlocks.filter((ab) => ab.blockId === block.id);
for (const { appId, orderIndex } of appsUsingBlock) {
// 2. Find the matching environment in the app based on the environment role
const blockBaseEnv = parsedJson.baseEnvironments.find((be) => be.id === subEnv.parentEnvironmentId);
// eslint-disable-next-line no-continue
if (!blockBaseEnv) continue;
const matchingAppEnv = parsedJson.baseEnvironments.find(
(be) => be.envParentId === appId && be.environmentRoleId === blockBaseEnv.environmentRoleId
);
// eslint-disable-next-line no-continue
if (!matchingAppEnv) continue;
// 3. Create a folder in the matching app environment
infisicalImportData.folders.push({
name: subEnv.subName,
parentFolderId: matchingAppEnv.id,
environmentId: matchingAppEnv.id,
id: `${subEnv.id}-${appId}` // Create unique ID for each app's copy of the branch
});
// 4. Process secrets in the block branch for this app
const branchSecrets = parsedJson.envs[subEnv.id]?.variables || {};
for (const [secretName, secretData] of Object.entries(branchSecrets)) {
if (secretData.inheritsEnvironmentId) {
const resolvedSecret = findRootInheritedSecret(secretData, secretName, parsedJson.envs);
// If the secret already exists in the environment, we need to check the orderIndex of the appBlock. The appBlock with the highest orderIndex should take precedence.
const preExistingSecretIndex = infisicalImportData.secrets.findIndex(
(s) => s.name === secretName && s.environmentId === matchingAppEnv.id
);
if (preExistingSecretIndex !== -1) {
const preExistingSecret = infisicalImportData.secrets[preExistingSecretIndex];
if (
preExistingSecret.appBlockOrderIndex !== undefined &&
orderIndex > preExistingSecret.appBlockOrderIndex
) {
// if the existing secret has a lower orderIndex, we should replace it
infisicalImportData.secrets[preExistingSecretIndex] = {
...preExistingSecret,
value: resolvedSecret.val || "",
appBlockOrderIndex: orderIndex
};
}
// eslint-disable-next-line no-continue
continue;
}
infisicalImportData.secrets.push({
id: randomUUID(),
name: secretName,
environmentId: matchingAppEnv.id,
value: resolvedSecret.val || "",
folderId: `${subEnv.id}-${appId}`,
appBlockOrderIndex: orderIndex
});
} else {
// If the secret already exists in the environment, we need to check the orderIndex of the appBlock. The appBlock with the highest orderIndex should take precedence.
const preExistingSecretIndex = infisicalImportData.secrets.findIndex(
(s) => s.name === secretName && s.environmentId === matchingAppEnv.id
);
if (preExistingSecretIndex !== -1) {
const preExistingSecret = infisicalImportData.secrets[preExistingSecretIndex];
if (
preExistingSecret.appBlockOrderIndex !== undefined &&
orderIndex > preExistingSecret.appBlockOrderIndex
) {
// if the existing secret has a lower orderIndex, we should replace it
infisicalImportData.secrets[preExistingSecretIndex] = {
...preExistingSecret,
value: secretData.val || "",
appBlockOrderIndex: orderIndex
};
}
// eslint-disable-next-line no-continue
continue;
}
infisicalImportData.secrets.push({
id: randomUUID(),
name: secretName,
environmentId: matchingAppEnv.id,
value: secretData.val || "",
folderId: `${subEnv.id}-${appId}`,
appBlockOrderIndex: orderIndex
});
}
}
}
}
}
};
const processBlocksForApp = (appIds: string[]) => {
for (const appId of appIds) {
const blocksInApp = parsedJson.appBlocks.filter((ab) => ab.appId === appId);
logger.info(
{
blocksInApp
},
"[processBlocksForApp]: Processing blocks for app"
);
for (const appBlock of blocksInApp) {
// 1. find all base environments for this block
const blockBaseEnvironments = parsedJson.baseEnvironments.filter((env) => env.envParentId === appBlock.blockId);
logger.info(
{
blockBaseEnvironments
},
"[processBlocksForApp]: Processing block base environments"
);
for (const blockBaseEnvironment of blockBaseEnvironments) {
// 2. find the corresponding environment that is not from the block
const matchingEnv = parsedJson.baseEnvironments.find(
(be) =>
be.environmentRoleId === blockBaseEnvironment.environmentRoleId && be.envParentId !== appBlock.blockId
);
if (!matchingEnv) {
throw new Error(`Could not find environment for block ${appBlock.blockId}`);
}
// 3. find all the secrets for this environment block
const blockSecrets = parsedJson.envs[blockBaseEnvironment.id].variables;
logger.info(
{
blockSecretsLength: Object.keys(blockSecrets).length
},
"[processBlocksForApp]: Processing block secrets"
);
// 4. process each secret
for (const secret of Object.keys(blockSecrets)) {
const selectedSecret = blockSecrets[secret];
if (selectedSecret.inheritsEnvironmentId) {
const resolvedSecret = findRootInheritedSecret(selectedSecret, secret, parsedJson.envs);
// If the secret already exists in the environment, we need to check the orderIndex of the appBlock. The appBlock with the highest orderIndex should take precedence.
const preExistingSecretIndex = infisicalImportData.secrets.findIndex(
(s) => s.name === secret && s.environmentId === matchingEnv.id
);
if (preExistingSecretIndex !== -1) {
const preExistingSecret = infisicalImportData.secrets[preExistingSecretIndex];
if (
preExistingSecret.appBlockOrderIndex !== undefined &&
appBlock.orderIndex > preExistingSecret.appBlockOrderIndex
) {
// if the existing secret has a lower orderIndex, we should replace it
infisicalImportData.secrets[preExistingSecretIndex] = {
...preExistingSecret,
value: selectedSecret.val || "",
appBlockOrderIndex: appBlock.orderIndex
};
}
// eslint-disable-next-line no-continue
continue;
}
infisicalImportData.secrets.push({
id: randomUUID(),
name: secret,
environmentId: matchingEnv.id,
value: resolvedSecret.val || "",
appBlockOrderIndex: appBlock.orderIndex
});
} else {
// If the secret already exists in the environment, we need to check the orderIndex of the appBlock. The appBlock with the highest orderIndex should take precedence.
const preExistingSecretIndex = infisicalImportData.secrets.findIndex(
(s) => s.name === secret && s.environmentId === matchingEnv.id
);
if (preExistingSecretIndex !== -1) {
const preExistingSecret = infisicalImportData.secrets[preExistingSecretIndex];
if (
preExistingSecret.appBlockOrderIndex !== undefined &&
appBlock.orderIndex > preExistingSecret.appBlockOrderIndex
) {
// if the existing secret has a lower orderIndex, we should replace it
infisicalImportData.secrets[preExistingSecretIndex] = {
...preExistingSecret,
value: selectedSecret.val || "",
appBlockOrderIndex: appBlock.orderIndex
};
}
// eslint-disable-next-line no-continue
continue;
}
infisicalImportData.secrets.push({
id: randomUUID(),
name: secret,
environmentId: matchingEnv.id,
value: selectedSecret.val || "",
appBlockOrderIndex: appBlock.orderIndex
});
}
}
}
}
}
};
processBranches();
processBlocksForApp(infisicalImportData.projects.map((app) => app.id));
for (const env of Object.keys(parsedJson.envs)) {
if (!env.includes("|")) {
const envData = parsedJson.envs[env];
for (const secret of Object.keys(envData.variables)) {
// Skip user-specific environments
// eslint-disable-next-line no-continue
if (env.includes("|")) continue;
const envData = parsedJson.envs[env];
const baseEnv = parsedJson.baseEnvironments.find((be) => be.id === env);
const subEnv = parsedJson.subEnvironments.find((se) => se.id === env);
// Skip if we can't find either a base environment or sub-environment
if (!baseEnv && !subEnv) {
logger.info(
{
envId: env
},
"[parseEnvKeyDataFn]: Could not find base or sub environment for env, skipping"
);
// eslint-disable-next-line no-continue
continue;
}
// If this is a base environment of a block, skip it (handled by processBlocksForApp)
if (baseEnv) {
const isBlock = parsedJson.appBlocks.some((block) => block.blockId === baseEnv.envParentId);
if (isBlock) {
logger.info(
{
envId: env,
baseEnv
},
"[parseEnvKeyDataFn]: Skipping block environment (handled separately)"
);
// eslint-disable-next-line no-continue
continue;
}
}
// Process each secret in this environment or branch
for (const [secretName, secretData] of Object.entries(envData.variables)) {
const environmentId = subEnv ? subEnv.parentEnvironmentId : env;
const indexOfExistingSecret = infisicalImportData.secrets.findIndex(
(s) => s.name === secretName && s.environmentId === environmentId
);
if (secretData.inheritsEnvironmentId) {
const resolvedSecret = findRootInheritedSecret(secretData, secretName, parsedJson.envs);
// Check if there's already a secret with this name in the environment, if there is, we should override it. Because if there's already one, we know its coming from a block.
// Variables from the normal environment should take precedence over variables from the block.
if (indexOfExistingSecret !== -1) {
// if a existing secret is found, we should replace it directly
const newSecret: (typeof infisicalImportData.secrets)[number] = {
...infisicalImportData.secrets[indexOfExistingSecret],
value: resolvedSecret.val || ""
};
infisicalImportData.secrets[indexOfExistingSecret] = newSecret;
// eslint-disable-next-line no-continue
continue;
}
infisicalImportData.secrets.push({
id: randomUUID(),
name: secret,
environmentId: env,
value: envData.variables[secret].val
name: secretName,
environmentId: subEnv ? subEnv.parentEnvironmentId : env,
value: resolvedSecret.val || "",
...(subEnv && { folderId: subEnv.id }) // Add folderId if this is a branch secret
});
} else {
// Check if there's already a secret with this name in the environment, if there is, we should override it. Because if there's already one, we know its coming from a block.
// Variables from the normal environment should take precedence over variables from the block.
if (indexOfExistingSecret !== -1) {
// if a existing secret is found, we should replace it directly
const newSecret: (typeof infisicalImportData.secrets)[number] = {
...infisicalImportData.secrets[indexOfExistingSecret],
value: secretData.val || ""
};
infisicalImportData.secrets[indexOfExistingSecret] = newSecret;
// eslint-disable-next-line no-continue
continue;
}
infisicalImportData.secrets.push({
id: randomUUID(),
name: secretName,
environmentId: subEnv ? subEnv.parentEnvironmentId : env,
value: secretData.val || "",
...(subEnv && { folderId: subEnv.id }) // Add folderId if this is a branch secret
});
}
}
@ -125,7 +488,17 @@ export const importDataIntoInfisicalFn = async ({
}
const originalToNewProjectId = new Map<string, string>();
const originalToNewEnvironmentId = new Map<string, string>();
const originalToNewEnvironmentId = new Map<
string,
{ envId: string; envSlug: string; rootFolderId: string; projectId: string }
>();
const originalToNewFolderId = new Map<
string,
{
folderId: string;
projectId: string;
}
>();
const projectsNotImported: string[] = [];
await projectDAL.transaction(async (tx) => {
@ -170,65 +543,161 @@ export const importDataIntoInfisicalFn = async ({
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
originalToNewEnvironmentId.set(environment.id, doc.slug);
originalToNewEnvironmentId.set(environment.id, {
envSlug: doc.slug,
envId: doc.id,
rootFolderId: folder.id,
projectId
});
}
}
if (data.folders) {
for await (const folder of data.folders) {
const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string);
if (!parentEnv) {
// eslint-disable-next-line no-continue
continue;
}
const newFolder = await folderDAL.create(
{
name: folder.name,
envId: parentEnv.envId,
parentId: parentEnv.rootFolderId
},
tx
);
originalToNewFolderId.set(folder.id, {
folderId: newFolder.id,
projectId: parentEnv.projectId
});
}
}
// Useful for debugging:
// console.log("data.secrets", data.secrets);
// console.log("data.folders", data.folders);
// console.log("data.environment", data.environments);
if (data.secrets && data.secrets.length > 0) {
const mappedToEnvironmentId = new Map<
string,
{
secretKey: string;
secretValue: string;
folderId?: string;
}[]
>();
for (const secret of data.secrets) {
if (!originalToNewEnvironmentId.get(secret.environmentId)) {
const targetId = secret.folderId || secret.environmentId;
// Skip if we can't find either an environment or folder mapping for this secret
if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) {
// eslint-disable-next-line no-continue
continue;
}
if (!mappedToEnvironmentId.has(secret.environmentId)) {
mappedToEnvironmentId.set(secret.environmentId, []);
if (!mappedToEnvironmentId.has(targetId)) {
mappedToEnvironmentId.set(targetId, []);
}
mappedToEnvironmentId.get(secret.environmentId)!.push({
mappedToEnvironmentId.get(targetId)!.push({
secretKey: secret.name,
secretValue: secret.value || ""
secretValue: secret.value || "",
folderId: secret.folderId
});
}
// for each of the mappedEnvironmentId
for await (const [envId, secrets] of mappedToEnvironmentId) {
const environment = data.environments.find((env) => env.id === envId);
const projectId = originalToNewProjectId.get(environment?.projectId as string)!;
for await (const [targetId, secrets] of mappedToEnvironmentId) {
logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId);
if (!projectId) {
throw new BadRequestError({ message: `Failed to import secret, project not found` });
let selectedFolder: TSecretFolders | undefined;
let selectedProjectId: string | undefined;
// Case 1: Secret belongs to a folder / branch / branch of a block
const foundFolder = originalToNewFolderId.get(targetId);
if (foundFolder) {
logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder");
selectedFolder = await folderDAL.findById(foundFolder.folderId, tx);
selectedProjectId = foundFolder.projectId;
} else {
logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment");
const environment = data.environments.find((env) => env.id === targetId);
if (!environment) {
logger.info(
{
targetId
},
"[importDataIntoInfisicalFn]: Could not find environment for secret"
);
// eslint-disable-next-line no-continue
continue;
}
const projectId = originalToNewProjectId.get(environment.projectId)!;
if (!projectId) {
throw new BadRequestError({ message: `Failed to import secret, project not found` });
}
const env = originalToNewEnvironmentId.get(targetId);
if (!env) {
logger.info(
{
targetId
},
"[importDataIntoInfisicalFn]: Could not find environment for secret"
);
// eslint-disable-next-line no-continue
continue;
}
const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx);
if (!folder) {
throw new NotFoundError({
message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`,
name: "Create secret"
});
}
selectedFolder = folder;
selectedProjectId = projectId;
}
if (!selectedFolder) {
throw new NotFoundError({
message: `Folder not found for the given environment slug & secret path`,
name: "CreateSecret"
});
}
if (!selectedProjectId) {
throw new NotFoundError({
message: `Project not found for the given environment slug & secret path`,
name: "CreateSecret"
});
}
const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.SecretManager,
projectId
projectId: selectedProjectId
},
tx
);
const envSlug = originalToNewEnvironmentId.get(envId)!;
const folder = await folderDAL.findBySecretPath(projectId, envSlug, "/", tx);
if (!folder)
throw new NotFoundError({
message: `Folder not found for the given environment slug (${envSlug}) & secret path (/)`,
name: "Create secret"
});
const secretBatches = chunkArray(secrets, 2500);
for await (const secretBatch of secretBatches) {
const secretsByKeys = await secretDAL.findBySecretKeys(
folder.id,
selectedFolder.id,
secretBatch.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
@ -242,7 +711,7 @@ export const importDataIntoInfisicalFn = async ({
}
await fnSecretBulkInsert({
inputSecrets: secretBatch.map((el) => {
const references = getAllNestedSecretReferences(el.secretValue);
const references = getAllSecretReferences(el.secretValue).nestedReferences;
return {
version: 1,
@ -254,7 +723,7 @@ export const importDataIntoInfisicalFn = async ({
type: SecretType.Shared
};
}),
folderId: folder.id,
folderId: selectedFolder.id,
secretDAL,
secretVersionDAL,
secretTagDAL,

View File

@ -31,7 +31,7 @@ export type TExternalMigrationQueueFactoryDep = {
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;

View File

@ -2,8 +2,16 @@ import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export type InfisicalImportData = {
projects: Array<{ name: string; id: string }>;
environments: Array<{ name: string; id: string; projectId: string }>;
secrets: Array<{ name: string; id: string; environmentId: string; value: string }>;
environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }>;
folders: Array<{ id: string; name: string; environmentId: string; parentFolderId?: string }>;
secrets: Array<{
id: string;
name: string;
environmentId: string;
value: string;
folderId?: string;
appBlockOrderIndex?: number; // Not used for infisical import, only used for building the import structure to determine which block(s) take precedence.
}>;
};
export type TImportEnvKeyDataCreate = {
@ -28,62 +36,62 @@ export type TEnvKeyExportJSON = {
org: {
id: string;
name: string;
settings: {
auth: {
inviteExpirationMs: number;
deviceGrantExpirationMs: number;
tokenExpirationMs: number;
};
crypto: {
requiresPassphrase: boolean;
requiresLockout: boolean;
};
envs: {
autoCaps: boolean;
autoCommitLocals: boolean;
};
};
};
// Apps are projects
apps: {
id: string;
name: string;
settings: Record<string, unknown>;
}[];
defaultOrgRoles: {
// Blocks are basically global projects that can be imported in other projects
blocks: {
id: string;
defaultName: string;
name: string;
}[];
defaultAppRoles: {
id: string;
defaultName: string;
appBlocks: {
appId: string;
blockId: string;
orderIndex: number;
}[];
defaultEnvironmentRoles: {
id: string;
defaultName: string;
settings: {
autoCommit: boolean;
};
}[];
nonDefaultEnvironmentRoles: {
id: string;
name: string;
}[];
baseEnvironments: {
id: string;
envParentId: string;
environmentRoleId: string;
settings: Record<string, unknown>;
}[];
orgUsers: {
// Branches for both blocks and apps
subEnvironments: {
id: string;
firstName: string;
lastName: string;
email: string;
provider: string;
orgRoleId: string;
uid: string;
envParentId: string;
environmentRoleId: string;
parentEnvironmentId: string;
subName: string;
}[];
envs: Record<
string,
{
variables: Record<string, { val: string }>;
inherits: Record<string, unknown>;
variables: Record<
string,
{
val?: string;
inheritsEnvironmentId?: string;
}
>;
inherits: Record<string, string[]>;
}
>;
};

View File

@ -158,6 +158,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
)
.select(
db.ref("id").withSchema(TableName.UserGroupMembership),
db.ref("createdAt").withSchema(TableName.UserGroupMembership),
db.ref("isGhost").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
@ -181,7 +182,18 @@ export const groupProjectDALFactory = (db: TDbClient) => {
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({
parentMapper: ({
email,
firstName,
username,
lastName,
publicKey,
isGhost,
id,
userId,
projectName,
createdAt
}) => ({
isGroupMember: true,
id,
userId,
@ -190,7 +202,8 @@ export const groupProjectDALFactory = (db: TDbClient) => {
id: projectId,
name: projectName
},
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
createdAt
}),
key: "id",
childrenMapper: [

View File

@ -98,6 +98,9 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
const removeExpiredTokens = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token started`);
const MAX_TTL = 315_360_000; // Maximum TTL value in seconds (10 years)
try {
const docs = (tx || db)(TableName.IdentityAccessToken)
.where({
@ -120,7 +123,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.whereNotNull("accessTokenLastRenewedAt")
// accessTokenLastRenewedAt + convert_integer_to_seconds(accessTokenTTL) < present_date
.andWhereRaw(
`"${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt" + make_interval(secs => "${TableName.IdentityAccessToken}"."accessTokenTTL") < NOW()`
`"${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt" + make_interval(secs => LEAST("${TableName.IdentityAccessToken}"."accessTokenTTL", ?)) < NOW()`,
[MAX_TTL]
);
})
.orWhere((qb3) => {
@ -128,7 +132,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.whereNull("accessTokenLastRenewedAt")
// created + convert_integer_to_seconds(accessTokenTTL) < present_date
.andWhereRaw(
`"${TableName.IdentityAccessToken}"."createdAt" + make_interval(secs => "${TableName.IdentityAccessToken}"."accessTokenTTL") < NOW()`
`"${TableName.IdentityAccessToken}"."createdAt" + make_interval(secs => LEAST("${TableName.IdentityAccessToken}"."accessTokenTTL", ?)) < NOW()`,
[MAX_TTL]
);
});
});

View File

@ -28,6 +28,7 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
const removeExpiredClientSecrets = async (tx?: Knex) => {
const BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
const MAX_TTL = 315_360_000; // Maximum TTL value in seconds (10 years)
let deletedClientSecret: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
@ -53,7 +54,8 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
void qb
.where("clientSecretTTL", ">", 0)
.andWhereRaw(
`"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => "${TableName.IdentityUaClientSecret}"."clientSecretTTL") < NOW()`
`"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => LEAST("${TableName.IdentityUaClientSecret}"."clientSecretTTL", ?)) < NOW()`,
[MAX_TTL]
);
})
.select("id")

View File

@ -1113,6 +1113,8 @@ export const getApps = async ({
});
case Integrations.AZURE_KEY_VAULT:
return [];
case Integrations.AZURE_APP_CONFIGURATION:
return [];
case Integrations.AWS_PARAMETER_STORE:
return [];
case Integrations.AWS_SECRET_MANAGER:

View File

@ -371,7 +371,8 @@ export const integrationAuthServiceFactory = ({
let accessId: string | undefined;
// this means its not access token based
if (
integrationAuth.integration === Integrations.AWS_SECRET_MANAGER &&
(integrationAuth.integration === Integrations.AWS_SECRET_MANAGER ||
integrationAuth.integration === Integrations.AWS_PARAMETER_STORE) &&
(shouldUseSecretV2Bridge
? integrationAuth.encryptedAwsAssumeIamRoleArn
: integrationAuth.awsAssumeIamRoleArnCipherText)

View File

@ -67,7 +67,8 @@ const getIntegrationSecretsV2 = async (
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
allowedImports: secretImports
secretImports,
hasSecretAccess: () => true
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {

View File

@ -33,7 +33,8 @@ export enum Integrations {
NORTHFLANK = "northflank",
HASURA_CLOUD = "hasura-cloud",
RUNDECK = "rundeck",
AZURE_DEVOPS = "azure-devops"
AZURE_DEVOPS = "azure-devops",
AZURE_APP_CONFIGURATION = "azure-app-configuration"
}
export enum IntegrationType {
@ -206,6 +207,15 @@ export const getIntegrationOptions = async () => {
clientId: appCfg.CLIENT_ID_AZURE,
docsLink: ""
},
{
name: "Azure App Configuration",
slug: "azure-app-configuration",
image: "Microsoft Azure.png",
isAvailable: true,
type: "oauth",
clientId: appCfg.CLIENT_ID_AZURE,
docsLink: ""
},
{
name: "Circle CI",
slug: "circleci",

View File

@ -24,6 +24,7 @@ import { Octokit } from "@octokit/rest";
import AWS, { AWSError } from "aws-sdk";
import { AxiosError } from "axios";
import { randomUUID } from "crypto";
import https from "https";
import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal";
import { z } from "zod";
@ -270,6 +271,180 @@ const syncSecretsGCPSecretManager = async ({
}
};
const syncSecretsAzureAppConfig = async ({
integration,
secrets,
accessToken,
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL
}: {
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<{ id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
}) => {
interface AzureAppConfigKeyValue {
key: string;
value: string;
}
const getCompleteAzureAppConfigValues = async (url: string) => {
let result: AzureAppConfigKeyValue[] = [];
while (url) {
const res = await request.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`
},
// we force IPV4 because docker setup fails with ipv6
httpsAgent: new https.Agent({
family: 4
})
});
result = result.concat(res.data.items);
url = res.data.nextLink;
}
return result;
};
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
const azureAppConfigSecrets = (
await getCompleteAzureAppConfigValues(
`${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix || ""}*`
)
).reduce(
(accum, entry) => {
accum[entry.key] = entry.value;
return accum;
},
{} as Record<string, string>
);
const secretsToAdd: { [key: string]: string } = {};
const secretsToUpdate: { [key: string]: string } = {};
Object.keys(azureAppConfigSecrets).forEach((key) => {
if (!integration.lastUsed) {
// first time using integration
// -> apply initial sync behavior
switch (metadata.initialSyncBehavior) {
case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
if (!(key in secrets)) {
secrets[key] = null;
}
break;
}
case IntegrationInitialSyncBehavior.PREFER_TARGET: {
if (!(key in secrets)) {
secretsToAdd[key] = azureAppConfigSecrets[key];
} else if (secrets[key]?.value !== azureAppConfigSecrets[key]) {
secretsToUpdate[key] = azureAppConfigSecrets[key];
}
secrets[key] = {
value: azureAppConfigSecrets[key]
};
break;
}
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
if (!(key in secrets)) {
secrets[key] = {
value: azureAppConfigSecrets[key]
};
secretsToAdd[key] = azureAppConfigSecrets[key];
}
break;
}
default: {
break;
}
}
} else if (!(key in secrets)) {
secrets[key] = null;
}
});
if (Object.keys(secretsToAdd).length) {
await createManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToAdd).map((key) => ({
secretName: key,
secretValue: secretsToAdd[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
if (Object.keys(secretsToUpdate).length) {
await updateManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToUpdate).map((key) => ({
secretName: key,
secretValue: secretsToUpdate[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
// create or update secrets on Azure App Config
for await (const key of Object.keys(secrets)) {
if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) {
await request.put(
`${integration.app}/kv/${key}?api-version=2023-11-01`,
{
value: secrets[key]?.value
},
{
headers: {
Authorization: `Bearer ${accessToken}`
},
// we force IPV4 because docker setup fails with ipv6
httpsAgent: new https.Agent({
family: 4
})
}
);
}
}
for await (const key of Object.keys(azureAppConfigSecrets)) {
if (!(key in secrets) || secrets[key] === null) {
// case: delete secret
await request.delete(`${integration.app}/kv/${key}?api-version=2023-11-01`, {
headers: {
Authorization: `Bearer ${accessToken}`
},
// we force IPV4 because docker setup fails with ipv6
httpsAgent: new https.Agent({
family: 4
})
});
}
}
await integrationDAL.updateById(integration.id, {
lastUsed: new Date()
});
};
/**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
*/
@ -555,24 +730,63 @@ const syncSecretsAWSParameterStore = async ({
secrets,
accessId,
accessToken,
projectId
projectId,
awsAssumeRoleArn
}: {
integration: TIntegrations & { secretPath: string; environment: { slug: string } };
secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null;
accessToken: string;
awsAssumeRoleArn: string | null;
projectId?: string;
}) => {
const appCfg = getConfig();
let response: { isSynced: boolean; syncMessage: string } | null = null;
if (!accessId) {
throw new Error("AWS access ID is required");
if (!accessId && !awsAssumeRoleArn) {
throw new Error("AWS access ID/AWS Assume Role is required");
}
let accessKeyId = "";
let secretAccessKey = "";
let sessionToken;
if (awsAssumeRoleArn) {
const client = new STSClient({
region: integration.region as string,
credentials:
appCfg.CLIENT_ID_AWS_INTEGRATION && appCfg.CLIENT_SECRET_AWS_INTEGRATION
? {
accessKeyId: appCfg.CLIENT_ID_AWS_INTEGRATION,
secretAccessKey: appCfg.CLIENT_SECRET_AWS_INTEGRATION
}
: undefined
});
const command = new AssumeRoleCommand({
RoleArn: awsAssumeRoleArn,
RoleSessionName: `infisical-parameter-store-${randomUUID()}`,
DurationSeconds: 900, // 15mins
ExternalId: projectId
});
const assumeRes = await client.send(command);
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
throw new Error("Failed to assume role");
}
accessKeyId = assumeRes.Credentials?.AccessKeyId;
secretAccessKey = assumeRes.Credentials?.SecretAccessKey;
sessionToken = assumeRes.Credentials?.SessionToken;
} else {
accessKeyId = accessId as string;
secretAccessKey = accessToken;
}
const config = new AWS.Config({
region: integration.region as string,
credentials: {
accessKeyId: accessId,
secretAccessKey: accessToken
accessKeyId,
secretAccessKey,
sessionToken
}
});
@ -4041,12 +4255,23 @@ export const syncIntegrationSecrets = async ({
accessToken
});
break;
case Integrations.AZURE_APP_CONFIGURATION:
await syncSecretsAzureAppConfig({
integration,
integrationDAL,
secrets,
accessToken,
createManySecretsRawFn,
updateManySecretsRawFn
});
break;
case Integrations.AWS_PARAMETER_STORE:
response = await syncSecretsAWSParameterStore({
integration,
secrets,
accessId,
accessToken,
awsAssumeRoleArn,
projectId
});
break;

View File

@ -131,6 +131,35 @@ const exchangeCodeAzure = async ({ code }: { code: string }) => {
};
};
const exchangeCodeAzureAppConfig = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_AZURE || !appCfg.CLIENT_SECRET_AZURE) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res = (
await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code,
scope: "https://azconfig.io/.default openid offline_access",
client_id: appCfg.CLIENT_ID_AZURE,
client_secret: appCfg.CLIENT_SECRET_AZURE,
redirect_uri: `${appCfg.SITE_URL}/integrations/azure-app-configuration/oauth2/callback`
})
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
};
};
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
@ -434,6 +463,10 @@ export const exchangeCode = async ({
return exchangeCodeAzure({
code
});
case Integrations.AZURE_APP_CONFIGURATION:
return exchangeCodeAzureAppConfig({
code
});
case Integrations.HEROKU:
return exchangeCodeHeroku({
code
@ -746,6 +779,7 @@ export const exchangeRefresh = async (
accessExpiresAt: Date;
}> => {
switch (integration) {
case Integrations.AZURE_APP_CONFIGURATION:
case Integrations.AZURE_KEY_VAULT:
return exchangeRefreshAzure({
refreshToken

View File

@ -90,7 +90,10 @@ export const integrationServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath })
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath
})
);
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
@ -167,7 +170,10 @@ export const integrationServiceFactory = ({
if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath })
subject(ProjectPermissionSub.Secrets, {
environment: newEnvironment,
secretPath: newSecretPath
})
);
}

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