Compare commits

..

183 Commits

Author SHA1 Message Date
bbe769a961 Increase SV migrate to 1000 & add billing page to Gamma 2024-02-22 07:30:26 +01:00
31cc3ece0c Update srp.ts 2024-02-22 07:01:03 +01:00
52cfa1ba39 Update UpgradeOverlay.tsx 2024-02-22 06:29:27 +01:00
4553c6bb37 Update scim-service.ts 2024-02-22 05:02:37 +01:00
554f0cfd00 Fix backend test 2024-02-22 05:00:40 +01:00
0a5112d302 Update run-backend-tests.yml 2024-02-22 05:00:40 +01:00
bdb0ed3e5e Removed redundantcies 2024-02-22 05:00:40 +01:00
7816d8593e Final changes 2024-02-22 05:00:40 +01:00
816c793ae3 Update index.tsx 2024-02-22 05:00:40 +01:00
9f0d09f8ed Update seed-data.ts 2024-02-22 05:00:40 +01:00
2cbd2ee75f Update project-router.ts 2024-02-22 05:00:40 +01:00
368974cf01 Moved 2024-02-22 05:00:40 +01:00
8be976a694 Get last 700 secret versions instead of 100 2024-02-22 05:00:40 +01:00
cab47d0b98 Update project-queue.ts 2024-02-22 05:00:40 +01:00
aa81711824 Fixed integrations & bulk update issue 2024-02-22 05:00:40 +01:00
10fbb99a15 Refactor migration to work with conflict/merge update logic 2024-02-22 05:00:40 +01:00
4657985468 Update project-service.ts 2024-02-22 05:00:40 +01:00
68ac1d285a Improved polling 2024-02-22 05:00:40 +01:00
fe7524fca1 Requested changes 2024-02-22 05:00:40 +01:00
bf9b47ad66 Requested changes 2024-02-22 05:00:40 +01:00
8e49825e16 Cleanup 2024-02-22 05:00:40 +01:00
27b4749205 More tests 2024-02-22 05:00:40 +01:00
5b1f07a661 Move migration to latest 2024-02-22 05:00:40 +01:00
50128bbac6 Test decrypt fix 2024-02-22 05:00:40 +01:00
debf80cfdc More 2024-02-22 05:00:40 +01:00
4ab47ca175 Fix for "random" crash on creation 2024-02-22 05:00:40 +01:00
021413fbd9 Update project-router.ts 2024-02-22 05:00:40 +01:00
8a39276e04 Fix for commonJS 2024-02-22 05:00:40 +01:00
b5e64bc8b8 Update srp.ts 2024-02-22 05:00:40 +01:00
faa842c3d2 Revert seed 2024-02-22 05:00:40 +01:00
28b24115b7 Move check to inside service 2024-02-22 05:00:40 +01:00
198dc05753 Allow getting bot, but not creating 2024-02-22 05:00:40 +01:00
178492e9bd Add logger to avoid crash 2024-02-22 05:00:40 +01:00
fb9cdb591c Error renaming 2024-02-22 05:00:40 +01:00
4c5100de6b Update project-key-service.ts 2024-02-22 05:00:40 +01:00
b587e6a4a1 Update models.ts 2024-02-22 05:00:40 +01:00
773756d731 Add transaction support 2024-02-22 05:00:40 +01:00
9efece1f01 Update models.ts 2024-02-22 05:00:40 +01:00
bb6e8b1a51 Finished migration 2024-02-22 05:00:40 +01:00
0f98fc94f0 Ghost user and migration finished! 2024-02-22 05:00:40 +01:00
7f1963f1ac Update index.ts 2024-02-22 05:00:39 +01:00
6064c393c6 Ghost 2024-02-22 05:00:39 +01:00
0cecf05a5b Fix project seed (seeds old projects that can be upgraded) 2024-02-22 05:00:39 +01:00
dc6497f9eb Update licence-fns.ts 2024-02-22 05:00:39 +01:00
e445970f36 More gohst user 2024-02-22 05:00:35 +01:00
c33741d588 Ghost user WIP 2024-02-22 05:00:26 +01:00
5dfc84190d Update bot-router.ts 2024-02-22 05:00:26 +01:00
a1d11c0fcd Fixes 2024-02-22 05:00:26 +01:00
863bbd420c Added DAL methods 2024-02-22 05:00:26 +01:00
4b37b2afba Wired most of the frontend to support ghost users 2024-02-22 05:00:26 +01:00
a366dbb16d Ghost user! 2024-02-22 05:00:26 +01:00
423ad49490 Helper functions for adding workspace members on serverside 2024-02-22 05:00:26 +01:00
2a4bda481d Crypto stuff 2024-02-22 05:00:26 +01:00
5b550a97a1 TS erros 2024-02-22 05:00:26 +01:00
0fa0e4eb0f Ghost user migration 2024-02-22 05:00:26 +01:00
65e3f0ec95 Update 3-project.ts 2024-02-22 05:00:26 +01:00
c20f6e51ae Update project-router.ts 2024-02-22 05:00:26 +01:00
cee8ead78a Update org-dal.ts 2024-02-22 05:00:26 +01:00
82fe0bb5c4 Update project-bot-service.ts 2024-02-22 05:00:26 +01:00
0b7efa57be Proper update project endpoint 2024-02-22 05:00:26 +01:00
9c11226b71 Renaming 2024-02-22 05:00:26 +01:00
ae3606c9fb Optional slug on create project 2024-02-22 05:00:26 +01:00
a0e25b8ea2 Describe 2024-02-22 05:00:26 +01:00
0931a17af5 Convert check to a standalone DAL operation 2024-02-22 05:00:26 +01:00
c16bf2afdb Removed unused invite-signup endpoint (finally) 2024-02-22 05:00:26 +01:00
04b4e80dd1 Documentation 2024-02-22 05:00:26 +01:00
f178220c5a Documentation 2024-02-22 05:00:26 +01:00
ed353d3263 Extra 2024-02-22 05:00:26 +01:00
ec6ec8813e Moved 2024-02-22 05:00:26 +01:00
3ea529d525 Update srp.ts 2024-02-22 05:00:26 +01:00
f35f10558b Get last 700 secret versions instead of 100 2024-02-22 05:00:26 +01:00
28287b8ed4 Update project-queue.ts 2024-02-22 05:00:26 +01:00
0f3ec51d14 Update project-queue.ts 2024-02-22 05:00:26 +01:00
75813deb81 Fixed integrations & bulk update issue 2024-02-22 05:00:26 +01:00
66e57d5d11 correct error log 2024-02-22 05:00:26 +01:00
fb2a213214 Refactor migration to work with conflict/merge update logic 2024-02-22 05:00:26 +01:00
c0b11b8350 improve the styling of the project upgrade banner 2024-02-22 05:00:26 +01:00
bea24d9654 Remove rate limiter 2024-02-22 05:00:26 +01:00
a7bc62f8e4 Akhil requested changes 2024-02-22 05:00:26 +01:00
2ef7e8f58e Improved invite user to project (even though this function isn't actually used.) 2024-02-22 05:00:26 +01:00
41d3b9314e Throw before completion (FOR TESTING!) 2024-02-22 05:00:26 +01:00
1e9d49008b Update project-service.ts 2024-02-22 05:00:26 +01:00
49d07a6762 Fixed versioning bug 2024-02-22 05:00:26 +01:00
9ce71371a9 cx -> twMerge 2024-02-22 05:00:26 +01:00
c1c66da92b Improved polling 2024-02-22 05:00:26 +01:00
4121c1d573 Requested changes 2024-02-22 05:00:26 +01:00
108f3cf117 Requested changes 2024-02-22 05:00:26 +01:00
a6e263eded Requested changes 2024-02-22 05:00:26 +01:00
419916ee0c Block secret mutations during upgrade 2024-02-22 05:00:25 +01:00
f7e6a96a02 Cleanup 2024-02-22 05:00:25 +01:00
b0356ba941 More tests 2024-02-22 05:00:25 +01:00
7ea5323a37 Update project-queue.ts 2024-02-22 05:00:25 +01:00
23e198d891 Update UpgradeProjectAlert.tsx 2024-02-22 05:00:25 +01:00
9f9849ccfd Move migration to latest 2024-02-22 05:00:25 +01:00
0c53eb8e22 Test decrypt fix 2024-02-22 05:00:25 +01:00
9b62937db2 More 2024-02-22 05:00:25 +01:00
ebb8d632c4 Fix for "random" crash on creation 2024-02-22 05:00:25 +01:00
43aae87fb0 Update project-router.ts 2024-02-22 05:00:25 +01:00
3415514fde Update project-router.ts 2024-02-22 05:00:25 +01:00
c0e0ddde76 Update project-router.ts 2024-02-22 05:00:25 +01:00
39ae66a84f Update project-router.ts 2024-02-22 05:00:25 +01:00
e8ec5b8b49 Update check-api-for-breaking-changes.yml 2024-02-22 05:00:25 +01:00
592271de3b Fix for commonJS 2024-02-22 05:00:25 +01:00
5680b984cf Update srp.ts 2024-02-22 05:00:25 +01:00
f378d6cc2b Update srp.ts 2024-02-22 05:00:25 +01:00
04c12d9a75 Revert seed 2024-02-22 05:00:25 +01:00
31b5f779fb Frontend bot logic 2024-02-22 05:00:25 +01:00
bb92cef764 Fix dummy signup project to support V2 2024-02-22 05:00:25 +01:00
6090f86b74 Move check to inside service 2024-02-22 05:00:25 +01:00
8c3569a047 Check if project is v2 before allowing a bot to be created 2024-02-22 05:00:25 +01:00
6fa11fe637 Allow getting bot, but not creating 2024-02-22 05:00:25 +01:00
9287eb7031 Increase random size 2024-02-22 05:00:25 +01:00
e54b261c0f Add new routes 2024-02-22 05:00:25 +01:00
60747b10b6 Error renaming 2024-02-22 05:00:25 +01:00
bf278355c4 Delete membership by email 2024-02-22 05:00:25 +01:00
d3d429db37 Delete membership by email 2024-02-22 05:00:25 +01:00
f2dcc83a56 New doc routes 2024-02-22 05:00:25 +01:00
26576b6bcd Update project-membership-types.ts 2024-02-22 05:00:25 +01:00
4cca82c3c8 New service for deleting memberships by email 2024-02-22 05:00:25 +01:00
1b82a157cc Find membership by email 2024-02-22 05:00:25 +01:00
5409cffe33 Update project-key-service.ts 2024-02-22 05:00:25 +01:00
45327f10b1 Update project-bot-service.ts 2024-02-22 05:00:25 +01:00
37645ba126 Update project-router.ts 2024-02-22 05:00:25 +01:00
858b49d766 Add delete memberships by email 2024-02-22 05:00:25 +01:00
a3a1a0007d Add auth methods 2024-02-22 05:00:25 +01:00
075f457bd1 Update models.ts 2024-02-22 05:00:25 +01:00
5156971d75 Add feature flag for showing upgrade project modal 2024-02-22 05:00:25 +01:00
8f3de3cc90 Small UI fixes on approvals page (color and capitalization and spelling) 2024-02-22 05:00:25 +01:00
69cba4e6c7 Make alert only visible to admins and add reloading on completion 2024-02-22 05:00:25 +01:00
6dcab6646c Make it impossible to log in with the ghost user 2024-02-22 05:00:25 +01:00
8e13eb6077 Update project-service.ts 2024-02-22 05:00:25 +01:00
819a9b8d27 Finish upgrade queue 2024-02-22 05:00:25 +01:00
ec3cf0208c Add transaction support 2024-02-22 05:00:25 +01:00
4aa5822ae2 Don't show ghost users 2024-02-22 05:00:25 +01:00
5364480ca2 Update models.ts 2024-02-22 05:00:25 +01:00
4802a36473 Finished migration 2024-02-22 05:00:25 +01:00
8333250b0b Ghost user and migration finished! 2024-02-22 05:00:25 +01:00
0cfab8ab6b Update index.ts 2024-02-22 05:00:25 +01:00
8fd99855bd Ghost 2024-02-22 05:00:25 +01:00
f2c36c58f9 Fix project seed (seeds old projects that can be upgraded) 2024-02-22 05:00:25 +01:00
f47fdfe386 Update licence-fns.ts 2024-02-22 05:00:25 +01:00
8a11eebab8 Typo 2024-02-22 05:00:14 +01:00
3b1fc4b156 More gohst user 2024-02-22 05:00:14 +01:00
84cab17f5c Ghost user WIP 2024-02-22 04:59:11 +01:00
db773864d5 Update project-router.ts 2024-02-22 04:59:11 +01:00
b9840ceba9 Add project role 2024-02-22 04:59:11 +01:00
729ec7866a Update bot-router.ts 2024-02-22 04:59:11 +01:00
a7140941ee Fixes 2024-02-22 04:59:11 +01:00
34d1bbc2ed Added SRP helpers to serverside 2024-02-22 04:59:11 +01:00
3ad0382cb0 Added DAL methods 2024-02-22 04:59:11 +01:00
ccc409e9cd Wired most of the frontend to support ghost users 2024-02-22 04:59:11 +01:00
fe21ba0e54 Ghost user! 2024-02-22 04:59:11 +01:00
80a802386c Helper functions for adding workspace members on serverside 2024-02-22 04:59:11 +01:00
aec0e86182 Crypto stuff 2024-02-22 04:59:11 +01:00
8e3cddc1ea TS erros 2024-02-22 04:59:11 +01:00
3612e5834c Ghost user migration 2024-02-22 04:59:11 +01:00
031a2416a9 Schemas (mostly linting) 2024-02-22 04:59:11 +01:00
2eb9592b1a Merge pull request #1424 from Infisical/scim
SCIM Provisioning
2024-02-21 17:10:30 -08:00
bbd9fa4a56 Correct SCIM Token TTL ms 2024-02-21 17:00:59 -08:00
318ad25c11 Merge remote-tracking branch 'origin' into scim 2024-02-21 12:46:37 -08:00
c372eb7d20 Update SCIM docs, disable user management in Infisical if SAML is enforced 2024-02-21 11:20:42 -08:00
68a99a0b32 Merge pull request #1414 from Infisical/snyk-upgrade-5f0df547c23dd0ff111f6ca3860ac3c2
[Snyk] Upgrade sharp from 0.32.6 to 0.33.2
2024-02-21 12:10:41 -05:00
7512231e20 Merge branch 'main' into snyk-upgrade-5f0df547c23dd0ff111f6ca3860ac3c2 2024-02-21 12:10:35 -05:00
f0e580d68b Merge pull request #1428 from Infisical/snyk-upgrade-19fed8b881f49aaf8794932ec4cd1934
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.485.0 to 3.502.0
2024-02-21 12:09:59 -05:00
116015d3cf Merge pull request #1415 from Infisical/snyk-upgrade-f0a7e4b81df465ae80db9e59002f75cc
[Snyk] Upgrade posthog-js from 1.100.0 to 1.103.0
2024-02-21 12:09:39 -05:00
308ff50197 Merge branch 'main' into snyk-upgrade-f0a7e4b81df465ae80db9e59002f75cc 2024-02-21 12:06:28 -05:00
9df5cbbe85 Merge pull request #1416 from Infisical/snyk-upgrade-785ded2802fbc87ba8e754f66e326fb6
[Snyk] Upgrade cookies from 0.8.0 to 0.9.1
2024-02-21 12:03:38 -05:00
a714a64bc2 Merge branch 'main' into snyk-upgrade-785ded2802fbc87ba8e754f66e326fb6 2024-02-21 12:00:34 -05:00
ea18d99793 Merge pull request #1429 from akhilmhdh/fix/admin-fail-redirect
fix(admin): resolved undefined on redirect after admin signup
2024-02-21 11:54:40 -05:00
7c098529f7 fix(admin): resolved undefined on redirect after admin signup 2024-02-21 14:09:28 +05:30
e20c623e91 fix: upgrade @aws-sdk/client-secrets-manager from 3.485.0 to 3.502.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.485.0 to 3.502.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-02-20 17:51:48 +00:00
3260932741 fix: resovled be test failing as reusable job 2024-02-20 22:39:14 +05:30
72d4490ee7 Fix lint/type issues 2024-02-19 15:04:42 -08:00
2336a7265b Merge remote-tracking branch 'origin' into scim 2024-02-19 14:41:46 -08:00
6f9b30b46e Minor UX adjustments to SCIM 2024-02-19 12:25:25 -08:00
7a65f8c837 Complete preliminary SCIM fns, add permissioning to SCIM, add docs for SCIM 2024-02-18 18:50:23 -08:00
bccbedfc31 fix: upgrade cookies from 0.8.0 to 0.9.1
Snyk has created this PR to upgrade cookies from 0.8.0 to 0.9.1.

See this package in npm:
https://www.npmjs.com/package/cookies

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-02-17 04:00:12 +00:00
0ab811194d fix: upgrade posthog-js from 1.100.0 to 1.103.0
Snyk has created this PR to upgrade posthog-js from 1.100.0 to 1.103.0.

See this package in npm:
https://www.npmjs.com/package/posthog-js

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-02-17 04:00:08 +00:00
7b54109168 fix: upgrade sharp from 0.32.6 to 0.33.2
Snyk has created this PR to upgrade sharp from 0.32.6 to 0.33.2.

See this package in npm:
https://www.npmjs.com/package/sharp

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-02-17 04:00:04 +00:00
e15ed4cc58 Merge remote-tracking branch 'origin' into scim 2024-02-14 12:30:38 -08:00
c73ee49425 Pass Okta SCIM 2.0 SPEC Test 2024-02-14 09:22:23 -08:00
3a7b697549 Make progress on SCIM 2024-02-12 09:18:33 -08:00
d5064fe75a Start SCIM functionality 2024-02-08 15:54:20 -08:00
156 changed files with 6059 additions and 918 deletions

View File

@ -72,4 +72,4 @@ jobs:
run: |
docker-compose -f "docker-compose.dev.yml" down
docker stop infisical-api
docker remove infisical-api
docker remove infisical-api

View File

@ -7,13 +7,8 @@ on:
jobs:
infisical-tests:
name: Run tests before deployment
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Run backend api integration tests
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone:
name: Build infisical standalone image postgres
runs-on: ubuntu-latest

View File

@ -44,4 +44,4 @@ jobs:
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker-compose -f "docker-compose.dev.yml" down

View File

@ -56,6 +56,7 @@ export default {
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
);
} catch (error) {
console.log("[TEST] Error setting up environment", error);
await db.destroy();
throw error;
}

View File

@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.485.0",
"@aws-sdk/client-secrets-manager": "^3.502.0",
"@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.4.1",
@ -356,22 +356,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/core": {
"version": "3.496.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.496.0.tgz",
"integrity": "sha512-yT+ug7Cw/3eJi7x2es0+46x12+cIJm5Xv+GPWsrTFD1TKgqO/VPEgfDtHFagDNbFmjNQA65Ygc/kEdIX9ICX/A==",
"dependencies": {
"@smithy/core": "^1.3.1",
"@smithy/protocol-http": "^3.1.1",
"@smithy/signature-v4": "^2.1.1",
"@smithy/smithy-client": "^2.3.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-env": {
"version": "3.496.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.496.0.tgz",
@ -677,49 +661,49 @@
}
},
"node_modules/@aws-sdk/client-secrets-manager": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.485.0.tgz",
"integrity": "sha512-TruRGEdTy1y/5ln1NcU5LvIZyK38O89zU9vCfNQIKwTSrpS0sDJQukjg8VfMC8gbqUUvXdiPcS61Fxr1WfWn7g==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.502.0.tgz",
"integrity": "sha512-ICU084A/EbYMqca6NVFqeMtHh+KCdn0H7UjARUy5ur1yOlXXvxqAJGtKZDYFjuEO08F30zbv7+4HCOy6yjOJ0Q==",
"dependencies": {
"@aws-crypto/sha256-browser": "3.0.0",
"@aws-crypto/sha256-js": "3.0.0",
"@aws-sdk/client-sts": "3.485.0",
"@aws-sdk/core": "3.485.0",
"@aws-sdk/credential-provider-node": "3.485.0",
"@aws-sdk/middleware-host-header": "3.485.0",
"@aws-sdk/middleware-logger": "3.485.0",
"@aws-sdk/middleware-recursion-detection": "3.485.0",
"@aws-sdk/middleware-signing": "3.485.0",
"@aws-sdk/middleware-user-agent": "3.485.0",
"@aws-sdk/region-config-resolver": "3.485.0",
"@aws-sdk/types": "3.485.0",
"@aws-sdk/util-endpoints": "3.485.0",
"@aws-sdk/util-user-agent-browser": "3.485.0",
"@aws-sdk/util-user-agent-node": "3.485.0",
"@smithy/config-resolver": "^2.0.23",
"@smithy/core": "^1.2.2",
"@smithy/fetch-http-handler": "^2.3.2",
"@smithy/hash-node": "^2.0.18",
"@smithy/invalid-dependency": "^2.0.16",
"@smithy/middleware-content-length": "^2.0.18",
"@smithy/middleware-endpoint": "^2.3.0",
"@smithy/middleware-retry": "^2.0.26",
"@smithy/middleware-serde": "^2.0.16",
"@smithy/middleware-stack": "^2.0.10",
"@smithy/node-config-provider": "^2.1.9",
"@smithy/node-http-handler": "^2.2.2",
"@smithy/protocol-http": "^3.0.12",
"@smithy/smithy-client": "^2.2.1",
"@smithy/types": "^2.8.0",
"@smithy/url-parser": "^2.0.16",
"@smithy/util-base64": "^2.0.1",
"@smithy/util-body-length-browser": "^2.0.1",
"@smithy/util-body-length-node": "^2.1.0",
"@smithy/util-defaults-mode-browser": "^2.0.24",
"@smithy/util-defaults-mode-node": "^2.0.32",
"@smithy/util-endpoints": "^1.0.8",
"@smithy/util-retry": "^2.0.9",
"@smithy/util-utf8": "^2.0.2",
"@aws-sdk/client-sts": "3.502.0",
"@aws-sdk/core": "3.496.0",
"@aws-sdk/credential-provider-node": "3.502.0",
"@aws-sdk/middleware-host-header": "3.502.0",
"@aws-sdk/middleware-logger": "3.502.0",
"@aws-sdk/middleware-recursion-detection": "3.502.0",
"@aws-sdk/middleware-signing": "3.502.0",
"@aws-sdk/middleware-user-agent": "3.502.0",
"@aws-sdk/region-config-resolver": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@aws-sdk/util-endpoints": "3.502.0",
"@aws-sdk/util-user-agent-browser": "3.502.0",
"@aws-sdk/util-user-agent-node": "3.502.0",
"@smithy/config-resolver": "^2.1.1",
"@smithy/core": "^1.3.1",
"@smithy/fetch-http-handler": "^2.4.1",
"@smithy/hash-node": "^2.1.1",
"@smithy/invalid-dependency": "^2.1.1",
"@smithy/middleware-content-length": "^2.1.1",
"@smithy/middleware-endpoint": "^2.4.1",
"@smithy/middleware-retry": "^2.1.1",
"@smithy/middleware-serde": "^2.1.1",
"@smithy/middleware-stack": "^2.1.1",
"@smithy/node-config-provider": "^2.2.1",
"@smithy/node-http-handler": "^2.3.1",
"@smithy/protocol-http": "^3.1.1",
"@smithy/smithy-client": "^2.3.1",
"@smithy/types": "^2.9.1",
"@smithy/url-parser": "^2.1.1",
"@smithy/util-base64": "^2.1.1",
"@smithy/util-body-length-browser": "^2.1.1",
"@smithy/util-body-length-node": "^2.2.1",
"@smithy/util-defaults-mode-browser": "^2.1.1",
"@smithy/util-defaults-mode-node": "^2.1.1",
"@smithy/util-endpoints": "^1.1.1",
"@smithy/util-retry": "^2.1.1",
"@smithy/util-utf8": "^2.1.1",
"tslib": "^2.5.0",
"uuid": "^8.3.2"
},
@ -736,112 +720,166 @@
}
},
"node_modules/@aws-sdk/client-sso": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.485.0.tgz",
"integrity": "sha512-apN2bEn0PZs0jD4jAfvwO3dlWqw9YIQJ6TAudM1bd3S5vzWqlBBcLfQpK6taHoQaI+WqgUWXLuOf7gRFbGXKPg==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.502.0.tgz",
"integrity": "sha512-OZAYal1+PQgUUtWiHhRayDtX0OD+XpXHKAhjYgEIPbyhQaCMp3/Bq1xDX151piWXvXqXLJHFKb8DUEqzwGO9QA==",
"dependencies": {
"@aws-crypto/sha256-browser": "3.0.0",
"@aws-crypto/sha256-js": "3.0.0",
"@aws-sdk/core": "3.485.0",
"@aws-sdk/middleware-host-header": "3.485.0",
"@aws-sdk/middleware-logger": "3.485.0",
"@aws-sdk/middleware-recursion-detection": "3.485.0",
"@aws-sdk/middleware-user-agent": "3.485.0",
"@aws-sdk/region-config-resolver": "3.485.0",
"@aws-sdk/types": "3.485.0",
"@aws-sdk/util-endpoints": "3.485.0",
"@aws-sdk/util-user-agent-browser": "3.485.0",
"@aws-sdk/util-user-agent-node": "3.485.0",
"@smithy/config-resolver": "^2.0.23",
"@smithy/core": "^1.2.2",
"@smithy/fetch-http-handler": "^2.3.2",
"@smithy/hash-node": "^2.0.18",
"@smithy/invalid-dependency": "^2.0.16",
"@smithy/middleware-content-length": "^2.0.18",
"@smithy/middleware-endpoint": "^2.3.0",
"@smithy/middleware-retry": "^2.0.26",
"@smithy/middleware-serde": "^2.0.16",
"@smithy/middleware-stack": "^2.0.10",
"@smithy/node-config-provider": "^2.1.9",
"@smithy/node-http-handler": "^2.2.2",
"@smithy/protocol-http": "^3.0.12",
"@smithy/smithy-client": "^2.2.1",
"@smithy/types": "^2.8.0",
"@smithy/url-parser": "^2.0.16",
"@smithy/util-base64": "^2.0.1",
"@smithy/util-body-length-browser": "^2.0.1",
"@smithy/util-body-length-node": "^2.1.0",
"@smithy/util-defaults-mode-browser": "^2.0.24",
"@smithy/util-defaults-mode-node": "^2.0.32",
"@smithy/util-endpoints": "^1.0.8",
"@smithy/util-retry": "^2.0.9",
"@smithy/util-utf8": "^2.0.2",
"@aws-sdk/core": "3.496.0",
"@aws-sdk/middleware-host-header": "3.502.0",
"@aws-sdk/middleware-logger": "3.502.0",
"@aws-sdk/middleware-recursion-detection": "3.502.0",
"@aws-sdk/middleware-user-agent": "3.502.0",
"@aws-sdk/region-config-resolver": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@aws-sdk/util-endpoints": "3.502.0",
"@aws-sdk/util-user-agent-browser": "3.502.0",
"@aws-sdk/util-user-agent-node": "3.502.0",
"@smithy/config-resolver": "^2.1.1",
"@smithy/core": "^1.3.1",
"@smithy/fetch-http-handler": "^2.4.1",
"@smithy/hash-node": "^2.1.1",
"@smithy/invalid-dependency": "^2.1.1",
"@smithy/middleware-content-length": "^2.1.1",
"@smithy/middleware-endpoint": "^2.4.1",
"@smithy/middleware-retry": "^2.1.1",
"@smithy/middleware-serde": "^2.1.1",
"@smithy/middleware-stack": "^2.1.1",
"@smithy/node-config-provider": "^2.2.1",
"@smithy/node-http-handler": "^2.3.1",
"@smithy/protocol-http": "^3.1.1",
"@smithy/smithy-client": "^2.3.1",
"@smithy/types": "^2.9.1",
"@smithy/url-parser": "^2.1.1",
"@smithy/util-base64": "^2.1.1",
"@smithy/util-body-length-browser": "^2.1.1",
"@smithy/util-body-length-node": "^2.2.1",
"@smithy/util-defaults-mode-browser": "^2.1.1",
"@smithy/util-defaults-mode-node": "^2.1.1",
"@smithy/util-endpoints": "^1.1.1",
"@smithy/util-retry": "^2.1.1",
"@smithy/util-utf8": "^2.1.1",
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/client-sts": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.485.0.tgz",
"integrity": "sha512-PI4q36kVF0fpIPZyeQhrwwJZ6SRkOGvU3rX5Qn4b5UY5X+Ct1aLhqSX8/OB372UZIcnh6eSvERu8POHleDO7Jw==",
"node_modules/@aws-sdk/client-sso-oidc": {
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.502.0.tgz",
"integrity": "sha512-Yc9tZqTOMWtdgpkrdjKShgWb9oKNsFQrItfoiN1xWDllaFFRPi2KTiZiR0AbSTrNasJy13d210DOxrIdte+kWQ==",
"dependencies": {
"@aws-crypto/sha256-browser": "3.0.0",
"@aws-crypto/sha256-js": "3.0.0",
"@aws-sdk/core": "3.485.0",
"@aws-sdk/credential-provider-node": "3.485.0",
"@aws-sdk/middleware-host-header": "3.485.0",
"@aws-sdk/middleware-logger": "3.485.0",
"@aws-sdk/middleware-recursion-detection": "3.485.0",
"@aws-sdk/middleware-user-agent": "3.485.0",
"@aws-sdk/region-config-resolver": "3.485.0",
"@aws-sdk/types": "3.485.0",
"@aws-sdk/util-endpoints": "3.485.0",
"@aws-sdk/util-user-agent-browser": "3.485.0",
"@aws-sdk/util-user-agent-node": "3.485.0",
"@smithy/config-resolver": "^2.0.23",
"@smithy/core": "^1.2.2",
"@smithy/fetch-http-handler": "^2.3.2",
"@smithy/hash-node": "^2.0.18",
"@smithy/invalid-dependency": "^2.0.16",
"@smithy/middleware-content-length": "^2.0.18",
"@smithy/middleware-endpoint": "^2.3.0",
"@smithy/middleware-retry": "^2.0.26",
"@smithy/middleware-serde": "^2.0.16",
"@smithy/middleware-stack": "^2.0.10",
"@smithy/node-config-provider": "^2.1.9",
"@smithy/node-http-handler": "^2.2.2",
"@smithy/protocol-http": "^3.0.12",
"@smithy/smithy-client": "^2.2.1",
"@smithy/types": "^2.8.0",
"@smithy/url-parser": "^2.0.16",
"@smithy/util-base64": "^2.0.1",
"@smithy/util-body-length-browser": "^2.0.1",
"@smithy/util-body-length-node": "^2.1.0",
"@smithy/util-defaults-mode-browser": "^2.0.24",
"@smithy/util-defaults-mode-node": "^2.0.32",
"@smithy/util-endpoints": "^1.0.8",
"@smithy/util-middleware": "^2.0.9",
"@smithy/util-retry": "^2.0.9",
"@smithy/util-utf8": "^2.0.2",
"@aws-sdk/client-sts": "3.502.0",
"@aws-sdk/core": "3.496.0",
"@aws-sdk/middleware-host-header": "3.502.0",
"@aws-sdk/middleware-logger": "3.502.0",
"@aws-sdk/middleware-recursion-detection": "3.502.0",
"@aws-sdk/middleware-signing": "3.502.0",
"@aws-sdk/middleware-user-agent": "3.502.0",
"@aws-sdk/region-config-resolver": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@aws-sdk/util-endpoints": "3.502.0",
"@aws-sdk/util-user-agent-browser": "3.502.0",
"@aws-sdk/util-user-agent-node": "3.502.0",
"@smithy/config-resolver": "^2.1.1",
"@smithy/core": "^1.3.1",
"@smithy/fetch-http-handler": "^2.4.1",
"@smithy/hash-node": "^2.1.1",
"@smithy/invalid-dependency": "^2.1.1",
"@smithy/middleware-content-length": "^2.1.1",
"@smithy/middleware-endpoint": "^2.4.1",
"@smithy/middleware-retry": "^2.1.1",
"@smithy/middleware-serde": "^2.1.1",
"@smithy/middleware-stack": "^2.1.1",
"@smithy/node-config-provider": "^2.2.1",
"@smithy/node-http-handler": "^2.3.1",
"@smithy/protocol-http": "^3.1.1",
"@smithy/smithy-client": "^2.3.1",
"@smithy/types": "^2.9.1",
"@smithy/url-parser": "^2.1.1",
"@smithy/util-base64": "^2.1.1",
"@smithy/util-body-length-browser": "^2.1.1",
"@smithy/util-body-length-node": "^2.2.1",
"@smithy/util-defaults-mode-browser": "^2.1.1",
"@smithy/util-defaults-mode-node": "^2.1.1",
"@smithy/util-endpoints": "^1.1.1",
"@smithy/util-retry": "^2.1.1",
"@smithy/util-utf8": "^2.1.1",
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"@aws-sdk/credential-provider-node": "*"
}
},
"node_modules/@aws-sdk/client-sts": {
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.502.0.tgz",
"integrity": "sha512-0q08gsvn6nuRqjK+i/e30PT/t7vvYwmGJS0PhJikZWv5yRDNSUxSYG0uDwKSbLDzmc2UX5+mLeyjPHlL4hbGlA==",
"dependencies": {
"@aws-crypto/sha256-browser": "3.0.0",
"@aws-crypto/sha256-js": "3.0.0",
"@aws-sdk/core": "3.496.0",
"@aws-sdk/middleware-host-header": "3.502.0",
"@aws-sdk/middleware-logger": "3.502.0",
"@aws-sdk/middleware-recursion-detection": "3.502.0",
"@aws-sdk/middleware-user-agent": "3.502.0",
"@aws-sdk/region-config-resolver": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@aws-sdk/util-endpoints": "3.502.0",
"@aws-sdk/util-user-agent-browser": "3.502.0",
"@aws-sdk/util-user-agent-node": "3.502.0",
"@smithy/config-resolver": "^2.1.1",
"@smithy/core": "^1.3.1",
"@smithy/fetch-http-handler": "^2.4.1",
"@smithy/hash-node": "^2.1.1",
"@smithy/invalid-dependency": "^2.1.1",
"@smithy/middleware-content-length": "^2.1.1",
"@smithy/middleware-endpoint": "^2.4.1",
"@smithy/middleware-retry": "^2.1.1",
"@smithy/middleware-serde": "^2.1.1",
"@smithy/middleware-stack": "^2.1.1",
"@smithy/node-config-provider": "^2.2.1",
"@smithy/node-http-handler": "^2.3.1",
"@smithy/protocol-http": "^3.1.1",
"@smithy/smithy-client": "^2.3.1",
"@smithy/types": "^2.9.1",
"@smithy/url-parser": "^2.1.1",
"@smithy/util-base64": "^2.1.1",
"@smithy/util-body-length-browser": "^2.1.1",
"@smithy/util-body-length-node": "^2.2.1",
"@smithy/util-defaults-mode-browser": "^2.1.1",
"@smithy/util-defaults-mode-node": "^2.1.1",
"@smithy/util-endpoints": "^1.1.1",
"@smithy/util-middleware": "^2.1.1",
"@smithy/util-retry": "^2.1.1",
"@smithy/util-utf8": "^2.1.1",
"fast-xml-parser": "4.2.5",
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"@aws-sdk/credential-provider-node": "*"
}
},
"node_modules/@aws-sdk/core": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.485.0.tgz",
"integrity": "sha512-Yvi80DQcbjkYCft471ClE3HuetuNVqntCs6eFOomDcrJaqdOFrXv2kJAxky84MRA/xb7bGlDGAPbTuj1ICputg==",
"version": "3.496.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.496.0.tgz",
"integrity": "sha512-yT+ug7Cw/3eJi7x2es0+46x12+cIJm5Xv+GPWsrTFD1TKgqO/VPEgfDtHFagDNbFmjNQA65Ygc/kEdIX9ICX/A==",
"dependencies": {
"@smithy/core": "^1.2.2",
"@smithy/protocol-http": "^3.0.12",
"@smithy/signature-v4": "^2.0.0",
"@smithy/smithy-client": "^2.2.1",
"@smithy/types": "^2.8.0",
"@smithy/core": "^1.3.1",
"@smithy/protocol-http": "^3.1.1",
"@smithy/signature-v4": "^2.1.1",
"@smithy/smithy-client": "^2.3.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -849,13 +887,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-env": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.485.0.tgz",
"integrity": "sha512-3XkFgwVU1XOB33dV7t9BKJ/ptdl2iS+0dxE7ecq8aqT2/gsfKmLCae1G17P8WmdD3z0kMDTvnqM2aWgUnSOkmg==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.502.0.tgz",
"integrity": "sha512-KIB8Ae1Z7domMU/jU4KiIgK4tmYgvuXlhR54ehwlVHxnEoFPoPuGHFZU7oFn79jhhSLUFQ1lRYMxP0cEwb7XeQ==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/property-provider": "^2.0.0",
"@smithy/types": "^2.8.0",
"@aws-sdk/types": "3.502.0",
"@smithy/property-provider": "^2.1.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -863,19 +901,20 @@
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.485.0.tgz",
"integrity": "sha512-cFYF/Bdw7EnT4viSxYpNIv3IBkri/Yb+JpQXl8uDq7bfVJfAN5qZmK07vRkg08xL6TC4F41wshhMSAucGdTwIw==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.502.0.tgz",
"integrity": "sha512-1wB/escbspUY6uRDEMp9AMMyypUSyuQ0AMO1yQNtXviV8cPf+CuRbqP/UVnimHO1RuX0n5BmjDVVjUIEU6kuGA==",
"dependencies": {
"@aws-sdk/credential-provider-env": "3.485.0",
"@aws-sdk/credential-provider-process": "3.485.0",
"@aws-sdk/credential-provider-sso": "3.485.0",
"@aws-sdk/credential-provider-web-identity": "3.485.0",
"@aws-sdk/types": "3.485.0",
"@smithy/credential-provider-imds": "^2.0.0",
"@smithy/property-provider": "^2.0.0",
"@smithy/shared-ini-file-loader": "^2.0.6",
"@smithy/types": "^2.8.0",
"@aws-sdk/client-sts": "3.502.0",
"@aws-sdk/credential-provider-env": "3.502.0",
"@aws-sdk/credential-provider-process": "3.502.0",
"@aws-sdk/credential-provider-sso": "3.502.0",
"@aws-sdk/credential-provider-web-identity": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@smithy/credential-provider-imds": "^2.2.1",
"@smithy/property-provider": "^2.1.1",
"@smithy/shared-ini-file-loader": "^2.3.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -883,20 +922,20 @@
}
},
"node_modules/@aws-sdk/credential-provider-node": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.485.0.tgz",
"integrity": "sha512-2DwzO2azkSzngifKDT61W/DL0tSzewuaFHiLJWdfc8Et3mdAQJ9x3KAj8u7XFpjIcGNqk7FiKjN+zeGUuNiEhA==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.502.0.tgz",
"integrity": "sha512-qg71UpYeFrjhu5hD+vdRqZ+EYFB11BeszsbfEJGaHhOMHmmTHNBaDAexW+bUnJSXcJL0a8vniCvca+rElbcAHQ==",
"dependencies": {
"@aws-sdk/credential-provider-env": "3.485.0",
"@aws-sdk/credential-provider-ini": "3.485.0",
"@aws-sdk/credential-provider-process": "3.485.0",
"@aws-sdk/credential-provider-sso": "3.485.0",
"@aws-sdk/credential-provider-web-identity": "3.485.0",
"@aws-sdk/types": "3.485.0",
"@smithy/credential-provider-imds": "^2.0.0",
"@smithy/property-provider": "^2.0.0",
"@smithy/shared-ini-file-loader": "^2.0.6",
"@smithy/types": "^2.8.0",
"@aws-sdk/credential-provider-env": "3.502.0",
"@aws-sdk/credential-provider-ini": "3.502.0",
"@aws-sdk/credential-provider-process": "3.502.0",
"@aws-sdk/credential-provider-sso": "3.502.0",
"@aws-sdk/credential-provider-web-identity": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@smithy/credential-provider-imds": "^2.2.1",
"@smithy/property-provider": "^2.1.1",
"@smithy/shared-ini-file-loader": "^2.3.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -904,14 +943,14 @@
}
},
"node_modules/@aws-sdk/credential-provider-process": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.485.0.tgz",
"integrity": "sha512-X9qS6ZO/rDKYDgWqD1YmSX7sAUUHax9HbXlgGiTTdtfhZvQh1ZmnH6wiPu5WNliafHZFtZT2W07kgrDLPld/Ug==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.502.0.tgz",
"integrity": "sha512-fJJowOjQ4infYQX0E1J3xFVlmuwEYJAFk0Mo1qwafWmEthsBJs+6BR2RiWDELHKrSK35u4Pf3fu3RkYuCtmQFw==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/property-provider": "^2.0.0",
"@smithy/shared-ini-file-loader": "^2.0.6",
"@smithy/types": "^2.8.0",
"@aws-sdk/types": "3.502.0",
"@smithy/property-provider": "^2.1.1",
"@smithy/shared-ini-file-loader": "^2.3.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -919,16 +958,16 @@
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.485.0.tgz",
"integrity": "sha512-l0oC8GTrWh+LFQQfSmG1Jai1PX7Mhj9arb/CaS1/tmeZE0hgIXW++tvljYs/Dds4LGXUlaWG+P7BrObf6OyIXA==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.502.0.tgz",
"integrity": "sha512-/2Nyvo+cWQpH283lmZBimTJ9JDhES9FzQUkhUXZgxQo3Ez4sguLVi2V9xoFFyG0cMff5fuNivdKHfj4FeMGjZw==",
"dependencies": {
"@aws-sdk/client-sso": "3.485.0",
"@aws-sdk/token-providers": "3.485.0",
"@aws-sdk/types": "3.485.0",
"@smithy/property-provider": "^2.0.0",
"@smithy/shared-ini-file-loader": "^2.0.6",
"@smithy/types": "^2.8.0",
"@aws-sdk/client-sso": "3.502.0",
"@aws-sdk/token-providers": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@smithy/property-provider": "^2.1.1",
"@smithy/shared-ini-file-loader": "^2.3.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -936,13 +975,14 @@
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.485.0.tgz",
"integrity": "sha512-WpBFZFE0iXtnibH5POMEKITj/hR0YV5l2n9p8BEvKjdJ63s3Xke1RN20ZdIyKDaRDwj8adnKDgNPEnAKdS4kLw==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.502.0.tgz",
"integrity": "sha512-veBAjDqjMMgA2Qxxf9ywDfHYLeJpaeHWLWCQ9XCHwJJ6ZIGWmAZPTq3he/UMr5JIQXooIccqqyqXMDIXPenXpA==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/property-provider": "^2.0.0",
"@smithy/types": "^2.8.0",
"@aws-sdk/client-sts": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@smithy/property-provider": "^2.1.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -950,13 +990,13 @@
}
},
"node_modules/@aws-sdk/middleware-host-header": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.485.0.tgz",
"integrity": "sha512-1mAUX9dQNGo2RIKseVj7SI/D5abQJQ/Os8hQ0NyVAyyVYF+Yjx5PphKgfhM5yoBwuwZUl6q71XPYEGNx7be6SA==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.502.0.tgz",
"integrity": "sha512-EjnG0GTYXT/wJBmm5/mTjDcAkzU8L7wQjOzd3FTXuTCNNyvAvwrszbOj5FlarEw5XJBbQiZtBs+I5u9+zy560w==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/protocol-http": "^3.0.12",
"@smithy/types": "^2.8.0",
"@aws-sdk/types": "3.502.0",
"@smithy/protocol-http": "^3.1.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -964,12 +1004,12 @@
}
},
"node_modules/@aws-sdk/middleware-logger": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.485.0.tgz",
"integrity": "sha512-O8IgJ0LHi5wTs5GlpI7nqmmSSagkVdd1shpGgQWY2h0kMSCII8CJZHBG97dlFFpGTvx5EDlhPNek7rl/6F4dRw==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.502.0.tgz",
"integrity": "sha512-FDyv6K4nCoHxbjLGS2H8ex8I0KDIiu4FJgVRPs140ZJy6gE5Pwxzv6YTzZGLMrnqcIs9gh065Lf6DjwMelZqaw==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/types": "^2.8.0",
"@aws-sdk/types": "3.502.0",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -977,13 +1017,13 @@
}
},
"node_modules/@aws-sdk/middleware-recursion-detection": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.485.0.tgz",
"integrity": "sha512-ZeVNATGNFcqkWDut3luVszROTUzkU5u+rJpB/xmeMoenlDAjPRiHt/ca3WkI5wAnIJ1VSNGpD2sOFLMCH+EWag==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.502.0.tgz",
"integrity": "sha512-hvbyGJbxeuezxOu8VfFmcV4ql1hKXLxHTe5FNYfEBat2KaZXVhc1Hg+4TvB06/53p+E8J99Afmumkqbxs2esUA==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/protocol-http": "^3.0.12",
"@smithy/types": "^2.8.0",
"@aws-sdk/types": "3.502.0",
"@smithy/protocol-http": "^3.1.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -991,16 +1031,16 @@
}
},
"node_modules/@aws-sdk/middleware-signing": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.485.0.tgz",
"integrity": "sha512-41xzT2p1sOibhsLkdE5rwPJkNbBtKD8Gp36/ySfu0KE415wfXKacElSVxAaBw39/j7iSWDYqqybeEYbAzk+3GQ==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.502.0.tgz",
"integrity": "sha512-4hF08vSzJ7L6sB+393gOFj3s2N6nLusYS0XrMW6wYNFU10IDdbf8Z3TZ7gysDJJHEGQPmTAesPEDBsasGWcMxg==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/property-provider": "^2.0.0",
"@smithy/protocol-http": "^3.0.12",
"@smithy/signature-v4": "^2.0.0",
"@smithy/types": "^2.8.0",
"@smithy/util-middleware": "^2.0.9",
"@aws-sdk/types": "3.502.0",
"@smithy/property-provider": "^2.1.1",
"@smithy/protocol-http": "^3.1.1",
"@smithy/signature-v4": "^2.1.1",
"@smithy/types": "^2.9.1",
"@smithy/util-middleware": "^2.1.1",
"tslib": "^2.5.0"
},
"engines": {
@ -1008,14 +1048,14 @@
}
},
"node_modules/@aws-sdk/middleware-user-agent": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.485.0.tgz",
"integrity": "sha512-CddCVOn+OPQ0CcchketIg+WF6v+MDLAf3GOYTR2htUxxIm7HABuRd6R3kvQ5Jny9CV8gMt22G1UZITsFexSJlQ==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.502.0.tgz",
"integrity": "sha512-TxbBZbRiXPH0AUxegqiNd9aM9zNSbfjtBs5MEfcBsweeT/B2O7K1EjP9+CkB8Xmk/5FLKhAKLr19b1TNoE27rw==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@aws-sdk/util-endpoints": "3.485.0",
"@smithy/protocol-http": "^3.0.12",
"@smithy/types": "^2.8.0",
"@aws-sdk/types": "3.502.0",
"@aws-sdk/util-endpoints": "3.502.0",
"@smithy/protocol-http": "^3.1.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -1023,14 +1063,15 @@
}
},
"node_modules/@aws-sdk/region-config-resolver": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.485.0.tgz",
"integrity": "sha512-2FB2EQ0sIE+YgFqGtkE1lDIMIL6nYe6MkOHBwBM7bommadKIrbbr2L22bPZGs3ReTsxiJabjzxbuCAVhrpHmhg==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.502.0.tgz",
"integrity": "sha512-mxmsX2AGgnSM+Sah7mcQCIneOsJQNiLX0COwEttuf8eO+6cLMAZvVudH3BnWTfea4/A9nuri9DLCqBvEmPrilg==",
"dependencies": {
"@smithy/node-config-provider": "^2.1.9",
"@smithy/types": "^2.8.0",
"@smithy/util-config-provider": "^2.1.0",
"@smithy/util-middleware": "^2.0.9",
"@aws-sdk/types": "3.502.0",
"@smithy/node-config-provider": "^2.2.1",
"@smithy/types": "^2.9.1",
"@smithy/util-config-provider": "^2.2.1",
"@smithy/util-middleware": "^2.1.1",
"tslib": "^2.5.0"
},
"engines": {
@ -1038,46 +1079,15 @@
}
},
"node_modules/@aws-sdk/token-providers": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.485.0.tgz",
"integrity": "sha512-kOXA1WKIVIFNRqHL8ynVZ3hCKLsgnEmGr2iDR6agDNw5fYIlCO/6N2xR6QdGcLTvUUbwOlz4OvKLUQnWMKAnnA==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.502.0.tgz",
"integrity": "sha512-RQgMgIXYlSf0xGl6EUeD+pqIPBlb7e29dbqHOBFc66hJVYUC2ULZX7Y+jLvcGIEaMiIaTPyvntZRFip+U+9hag==",
"dependencies": {
"@aws-crypto/sha256-browser": "3.0.0",
"@aws-crypto/sha256-js": "3.0.0",
"@aws-sdk/middleware-host-header": "3.485.0",
"@aws-sdk/middleware-logger": "3.485.0",
"@aws-sdk/middleware-recursion-detection": "3.485.0",
"@aws-sdk/middleware-user-agent": "3.485.0",
"@aws-sdk/region-config-resolver": "3.485.0",
"@aws-sdk/types": "3.485.0",
"@aws-sdk/util-endpoints": "3.485.0",
"@aws-sdk/util-user-agent-browser": "3.485.0",
"@aws-sdk/util-user-agent-node": "3.485.0",
"@smithy/config-resolver": "^2.0.23",
"@smithy/fetch-http-handler": "^2.3.2",
"@smithy/hash-node": "^2.0.18",
"@smithy/invalid-dependency": "^2.0.16",
"@smithy/middleware-content-length": "^2.0.18",
"@smithy/middleware-endpoint": "^2.3.0",
"@smithy/middleware-retry": "^2.0.26",
"@smithy/middleware-serde": "^2.0.16",
"@smithy/middleware-stack": "^2.0.10",
"@smithy/node-config-provider": "^2.1.9",
"@smithy/node-http-handler": "^2.2.2",
"@smithy/property-provider": "^2.0.0",
"@smithy/protocol-http": "^3.0.12",
"@smithy/shared-ini-file-loader": "^2.0.6",
"@smithy/smithy-client": "^2.2.1",
"@smithy/types": "^2.8.0",
"@smithy/url-parser": "^2.0.16",
"@smithy/util-base64": "^2.0.1",
"@smithy/util-body-length-browser": "^2.0.1",
"@smithy/util-body-length-node": "^2.1.0",
"@smithy/util-defaults-mode-browser": "^2.0.24",
"@smithy/util-defaults-mode-node": "^2.0.32",
"@smithy/util-endpoints": "^1.0.8",
"@smithy/util-retry": "^2.0.9",
"@smithy/util-utf8": "^2.0.2",
"@aws-sdk/client-sso-oidc": "3.502.0",
"@aws-sdk/types": "3.502.0",
"@smithy/property-provider": "^2.1.1",
"@smithy/shared-ini-file-loader": "^2.3.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -1085,11 +1095,11 @@
}
},
"node_modules/@aws-sdk/types": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.485.0.tgz",
"integrity": "sha512-+QW32YQdvZRDOwrAQPo/qCyXoSjgXB6RwJwCwkd8ebJXRXw6tmGKIHaZqYHt/LtBymvnaBgBBADNa4+qFvlOFw==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.502.0.tgz",
"integrity": "sha512-M0DSPYe/gXhwD2QHgoukaZv5oDxhW3FfvYIrJptyqUq3OnPJBcDbihHjrE0PBtfh/9kgMZT60/fQ2NVFANfa2g==",
"dependencies": {
"@smithy/types": "^2.8.0",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {
@ -1097,12 +1107,13 @@
}
},
"node_modules/@aws-sdk/util-endpoints": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.485.0.tgz",
"integrity": "sha512-dTd642F7nJisApF8YjniqQ6U59CP/DCtar11fXf1nG9YNBCBsNNVw5ZfZb5nSNzaIdy27mQioWTCV18JEj1mxg==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.502.0.tgz",
"integrity": "sha512-6LKFlJPp2J24r1Kpfoz5ESQn+1v5fEjDB3mtUKRdpwarhm3syu7HbKlHCF3KbcCOyahobvLvhoedT78rJFEeeg==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/util-endpoints": "^1.0.8",
"@aws-sdk/types": "3.502.0",
"@smithy/types": "^2.9.1",
"@smithy/util-endpoints": "^1.1.1",
"tslib": "^2.5.0"
},
"engines": {
@ -1121,24 +1132,24 @@
}
},
"node_modules/@aws-sdk/util-user-agent-browser": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.485.0.tgz",
"integrity": "sha512-QliWbjg0uOhGTcWgWTKPMY0SBi07g253DjwrCINT1auqDrdQPxa10xozpZExBYjAK2KuhYDNUzni127ae6MHOw==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.502.0.tgz",
"integrity": "sha512-v8gKyCs2obXoIkLETAeEQ3AM+QmhHhst9xbM1cJtKUGsRlVIak/XyyD+kVE6kmMm1cjfudHpHKABWk9apQcIZQ==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/types": "^2.8.0",
"@aws-sdk/types": "3.502.0",
"@smithy/types": "^2.9.1",
"bowser": "^2.11.0",
"tslib": "^2.5.0"
}
},
"node_modules/@aws-sdk/util-user-agent-node": {
"version": "3.485.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.485.0.tgz",
"integrity": "sha512-QF+aQ9jnDlPUlFBxBRqOylPf86xQuD3aEPpOErR+50qJawVvKa94uiAFdvtI9jv6hnRZmuFsTj2rsyytnbAYBA==",
"version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.502.0.tgz",
"integrity": "sha512-9RjxpkGZKbTdl96tIJvAo+vZoz4P/cQh36SBUt9xfRfW0BtsaLyvSrvlR5wyUYhvRcC12Axqh/8JtnAPq//+Vw==",
"dependencies": {
"@aws-sdk/types": "3.485.0",
"@smithy/node-config-provider": "^2.1.9",
"@smithy/types": "^2.8.0",
"@aws-sdk/types": "3.502.0",
"@smithy/node-config-provider": "^2.2.1",
"@smithy/types": "^2.9.1",
"tslib": "^2.5.0"
},
"engines": {

View File

@ -70,7 +70,7 @@
"vitest": "^1.2.2"
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.485.0",
"@aws-sdk/client-secrets-manager": "^3.502.0",
"@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.4.1",

View File

@ -6,6 +6,7 @@ import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
@ -105,6 +106,7 @@ declare module "fastify" {
secretRotation: TSecretRotationServiceFactory;
snapshot: TSecretSnapshotServiceFactory;
saml: TSamlConfigServiceFactory;
scim: TScimServiceFactory;
auditLog: TAuditLogServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;

View File

@ -83,6 +83,9 @@ import {
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
TScimTokens,
TScimTokensInsert,
TScimTokensUpdate,
TSecretApprovalPolicies,
TSecretApprovalPoliciesApprovers,
TSecretApprovalPoliciesApproversInsert,
@ -262,6 +265,7 @@ declare module "knex/types/tables" {
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate
>;
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,
TSecretApprovalPoliciesInsert,

View File

@ -0,0 +1,31 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ScimToken))) {
await knex.schema.createTable(TableName.ScimToken, (t) => {
t.string("id", 36).primary().defaultTo(knex.fn.uuid());
t.bigInteger("ttlDays").defaultTo(365).notNullable();
t.string("description").notNullable();
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await knex.schema.alterTable(TableName.Organization, (t) => {
t.boolean("scimEnabled").defaultTo(false);
});
await createOnUpdateTrigger(knex, TableName.ScimToken);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ScimToken);
await dropOnUpdateTrigger(knex, TableName.ScimToken);
await knex.schema.alterTable(TableName.Organization, (t) => {
t.dropColumn("scimEnabled");
});
}

View File

@ -0,0 +1,39 @@
import { Knex } from "knex";
import { ProjectVersion, TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "isGhost");
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
if (!hasGhostUserColumn) {
await knex.schema.alterTable(TableName.Users, (t) => {
t.boolean("isGhost").defaultTo(false).notNullable();
});
}
if (!hasProjectVersionColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.integer("version").defaultTo(ProjectVersion.V1).notNullable();
t.string("upgradeStatus").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "isGhost");
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
if (hasGhostUserColumn) {
await knex.schema.alterTable(TableName.Users, (t) => {
t.dropColumn("isGhost");
});
}
if (hasProjectVersionColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("version");
t.dropColumn("upgradeStatus");
});
}
}

View File

@ -26,6 +26,7 @@ export * from "./project-memberships";
export * from "./project-roles";
export * from "./projects";
export * from "./saml-configs";
export * from "./scim-tokens";
export * from "./secret-approval-policies";
export * from "./secret-approval-policies-approvers";
export * from "./secret-approval-request-secret-tags";

View File

@ -40,6 +40,7 @@ export enum TableName {
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
ScimToken = "scim_tokens",
SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalRequest = "secret_approval_requests",
@ -111,6 +112,17 @@ export enum SecretType {
Personal = "personal"
}
export enum ProjectVersion {
V1 = 1,
V2 = 2
}
export enum ProjectUpgradeStatus {
InProgress = "IN_PROGRESS",
// Completed -> Will be null if completed. So a completed status is not needed
Failed = "FAILED"
}
export enum IdentityAuthMethod {
Univeral = "universal-auth"
}

View File

@ -14,7 +14,8 @@ export const OrganizationsSchema = z.object({
slug: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
authEnforced: z.boolean().default(false).nullable().optional()
authEnforced: z.boolean().default(false).nullable().optional(),
scimEnabled: z.boolean().default(false).nullable().optional()
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@ -14,7 +14,9 @@ export const ProjectsSchema = z.object({
autoCapitalization: z.boolean().default(true).nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
version: z.number().default(1),
upgradeStatus: z.string().nullable().optional()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ScimTokensSchema = z.object({
id: z.string(),
ttlDays: z.coerce.number().default(365),
description: z.string(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TScimTokens = z.infer<typeof ScimTokensSchema>;
export type TScimTokensInsert = Omit<TScimTokens, TImmutableDBKeys>;
export type TScimTokensUpdate = Partial<Omit<TScimTokens, TImmutableDBKeys>>;

View File

@ -19,7 +19,8 @@ export const UsersSchema = z.object({
mfaMethods: z.string().array().nullable().optional(),
devices: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
isGhost: z.boolean().default(false)
});
export type TUsers = z.infer<typeof UsersSchema>;

View File

@ -1,3 +1,4 @@
/* eslint-disable import/no-mutable-exports */
import crypto from "node:crypto";
import argon2, { argon2id } from "argon2";
@ -15,9 +16,12 @@ import {
import { TSecrets, TUserEncryptionKeys } from "./schemas";
export let userPrivateKey: string | undefined;
export let userPublicKey: string | undefined;
export const seedData1 = {
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
email: "test@localhost.local",
email: process.env.TEST_USER_EMAIL || "test@localhost.local",
password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
organization: {
id: "180870b7-f464-4740-8ffe-9d11c9245ea7",
@ -42,6 +46,12 @@ export const seedData1 = {
},
token: {
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
},
// We set these values during user creation, and later re-use them during project seeding.
encryptionKeys: {
publicKey: "",
privateKey: ""
}
};

View File

@ -3,6 +3,7 @@ import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router";
import { registerSamlRouter } from "./saml-router";
import { registerScimRouter } from "./scim-router";
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
@ -33,6 +34,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
prefix: "/secret-rotation-providers"
});
await server.register(registerSamlRouter, { prefix: "/sso" });
await server.register(registerScimRouter, { prefix: "/scim" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" });

View File

@ -0,0 +1,331 @@
import { z } from "zod";
import { ScimTokensSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerScimRouter = async (server: FastifyZodProvider) => {
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, function (req, body, done) {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
server.route({
url: "/scim-tokens",
method: "POST",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
organizationId: z.string().trim(),
description: z.string().trim().default(""),
ttlDays: z.number().min(0).default(0)
}),
response: {
200: z.object({
scimToken: z.string().trim()
})
}
},
handler: async (req) => {
const { scimToken } = await server.services.scim.createScimToken({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId,
description: req.body.description,
ttlDays: req.body.ttlDays
});
return { scimToken };
}
});
server.route({
url: "/scim-tokens",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
querystring: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.object({
scimTokens: z.array(ScimTokensSchema)
})
}
},
handler: async (req) => {
const scimTokens = await server.services.scim.listScimTokens({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.query.organizationId
});
return { scimTokens };
}
});
server.route({
url: "/scim-tokens/:scimTokenId",
method: "DELETE",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
scimTokenId: z.string().trim()
}),
response: {
200: z.object({
scimToken: ScimTokensSchema
})
}
},
handler: async (req) => {
const scimToken = await server.services.scim.deleteScimToken({
scimTokenId: req.params.scimTokenId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId
});
return { scimToken };
}
});
// SCIM server endpoints
server.route({
url: "/Users",
method: "GET",
schema: {
querystring: z.object({
startIndex: z.coerce.number().default(1),
count: z.coerce.number().default(20),
filter: z.string().trim().optional()
}),
response: {
200: z.object({
Resources: z.array(
z.object({
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
totalResults: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const users = await req.server.services.scim.listScimUsers({
offset: req.query.startIndex,
limit: req.query.count,
filter: req.query.filter,
orgId: req.permission.orgId as string
});
return users;
}
});
server.route({
url: "/Users/:userId",
method: "GET",
schema: {
params: z.object({
userId: z.string().trim()
}),
response: {
201: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.getScimUser({
userId: req.params.userId,
orgId: req.permission.orgId as string
});
return user;
}
});
server.route({
url: "/Users",
method: "POST",
schema: {
body: z.object({
schemas: z.array(z.string()),
userName: z.string().trim().email(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
// emails: z.array( // optional?
// z.object({
// primary: z.boolean(),
// value: z.string().email(),
// type: z.string().trim()
// })
// ),
// displayName: z.string().trim(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim().email(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.createScimUser({
email: req.body.userName,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
orgId: req.permission.orgId as string
});
return user;
}
});
server.route({
url: "/Users/:userId",
method: "PATCH",
schema: {
params: z.object({
userId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
Operations: z.array(
z.object({
op: z.string().trim(),
path: z.string().trim().optional(),
value: z.union([
z.object({
active: z.boolean()
}),
z.string().trim()
])
})
)
}),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.updateScimUser({
userId: req.params.userId,
orgId: req.permission.orgId as string,
operations: req.body.Operations
});
return user;
}
});
server.route({
url: "/Users/:userId",
method: "PUT",
schema: {
params: z.object({
userId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
displayName: z.string().trim(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({
userId: req.params.userId,
orgId: req.permission.orgId as string,
active: req.body.active
});
return user;
}
});
};

View File

@ -58,6 +58,7 @@ export const auditLogServiceFactory = ({
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
}
return auditLogQueue.pushToLog(data);
};

View File

@ -15,7 +15,7 @@ export type TListProjectAuditLogDTO = {
export type TCreateAuditLogDTO = {
event: Event;
actor: UserActor | IdentityActor | ServiceActor;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
orgId?: string;
projectId?: string;
} & BaseAuthData;
@ -105,6 +105,8 @@ interface IdentityActorMetadata {
name: string;
}
interface ScimClientActorMetadata {}
export interface UserActor {
type: ActorType.USER;
metadata: UserActorMetadata;
@ -120,7 +122,12 @@ export interface IdentityActor {
metadata: IdentityActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor;
export interface ScimClientActor {
type: ActorType.SCIM_CLIENT;
metadata: ScimClientActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;

View File

@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogs: false,
auditLogsRetentionDays: 0,
samlSSO: false,
scim: false,
status: null,
trial_end: null,
has_used_trial: true,

View File

@ -25,6 +25,7 @@ export type TFeatureSet = {
auditLogs: false;
auditLogsRetentionDays: 0;
samlSSO: false;
scim: false;
status: null;
trial_end: null;
has_used_trial: true;

View File

@ -16,6 +16,7 @@ export enum OrgPermissionSubjects {
Settings = "settings",
IncidentAccount = "incident-contact",
Sso = "sso",
Scim = "scim",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
@ -29,6 +30,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
@ -69,6 +71,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);

View File

@ -195,7 +195,7 @@ export const samlConfigServiceFactory = ({
updateQuery.certTag = certTag;
}
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
await orgDAL.updateById(orgId, { authEnforced: false });
await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false });
return ssoConfig;
};
@ -338,7 +338,8 @@ export const samlConfigServiceFactory = ({
email,
firstName,
lastName,
authMethods: [AuthMethod.EMAIL]
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);

View File

@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TScimDALFactory = ReturnType<typeof scimDALFactory>;
export const scimDALFactory = (db: TDbClient) => {
const scimTokenOrm = ormify(db, TableName.ScimToken);
return scimTokenOrm;
};

View File

@ -0,0 +1,58 @@
import { TListScimUsers, TScimUser } from "./scim-types";
export const buildScimUserList = ({
scimUsers,
offset,
limit
}: {
scimUsers: TScimUser[];
offset: number;
limit: number;
}): TListScimUsers => {
return {
Resources: scimUsers,
itemsPerPage: limit,
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
startIndex: offset,
totalResults: scimUsers.length
};
};
export const buildScimUser = ({
userId,
firstName,
lastName,
email,
active
}: {
userId: string;
firstName: string;
lastName: string;
email: string;
active: boolean;
}): TScimUser => {
return {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
id: userId,
userName: email,
displayName: `${firstName} ${lastName}`,
name: {
givenName: firstName,
middleName: null,
familyName: lastName
},
emails: [
{
primary: true,
value: email,
type: "work"
}
],
active,
groups: [],
meta: {
resourceType: "User",
location: null
}
};
};

View File

@ -0,0 +1,431 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembership } from "@app/services/org/org-fns";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { buildScimUser, buildScimUserList } from "./scim-fns";
import {
TCreateScimTokenDTO,
TCreateScimUserDTO,
TDeleteScimTokenDTO,
TGetScimUserDTO,
TListScimUsers,
TListScimUsersDTO,
TReplaceScimUserDTO,
TScimTokenJwtPayload,
TUpdateScimUserDTO
} from "./scim-types";
type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
>;
projectDAL: Pick<TProjectDALFactory, "find">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: TSmtpService;
};
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
export const scimServiceFactory = ({
licenseService,
scimDAL,
userDAL,
orgDAL,
projectDAL,
projectMembershipDAL,
permissionService,
smtpService
}: TScimServiceFactoryDep) => {
const createScimToken = async ({ actor, actorId, actorOrgId, orgId, description, ttlDays }: TCreateScimTokenDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(orgId);
if (!plan.scim)
throw new BadRequestError({
message: "Failed to create a SCIM token due to plan restriction. Upgrade plan to create a SCIM token."
});
const appCfg = getConfig();
const scimTokenData = await scimDAL.create({
orgId,
description,
ttlDays
});
const scimToken = jwt.sign(
{
scimTokenId: scimTokenData.id,
authTokenType: AuthTokenType.SCIM_TOKEN
},
appCfg.AUTH_SECRET
);
return { scimToken };
};
const listScimTokens = async ({ actor, actorId, actorOrgId, orgId }: TOrgPermission) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(orgId);
if (!plan.scim)
throw new BadRequestError({
message: "Failed to get SCIM tokens due to plan restriction. Upgrade plan to get SCIM tokens."
});
const scimTokens = await scimDAL.find({ orgId });
return scimTokens;
};
const deleteScimToken = async ({ scimTokenId, actor, actorId, actorOrgId }: TDeleteScimTokenDTO) => {
let scimToken = await scimDAL.findById(scimTokenId);
if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" });
const { permission } = await permissionService.getOrgPermission(actor, actorId, scimToken.orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(scimToken.orgId);
if (!plan.scim)
throw new BadRequestError({
message: "Failed to delete the SCIM token due to plan restriction. Upgrade plan to delete the SCIM token."
});
scimToken = await scimDAL.deleteById(scimTokenId);
return scimToken;
};
// SCIM server endpoints
const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const parseFilter = (filterToParse: string | undefined) => {
if (!filterToParse) return {};
const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
let attributeName = parsedName;
if (parsedName === "userName") {
attributeName = "email";
}
return { [attributeName]: parsedValue };
};
const findOpts = {
...(offset && { offset }),
...(limit && { limit })
};
const users = await orgDAL.findMembership(
{
orgId,
...parseFilter(filter)
},
findOpts
);
const scimUsers = users.map(({ userId, firstName, lastName, email }) =>
buildScimUser({
userId: userId ?? "",
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: true
})
);
return buildScimUserList({
scimUsers,
offset,
limit
});
};
const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
orgId
})
.catch(() => {
throw new ScimRequestError({
detail: "User not found",
status: 404
});
});
if (!membership)
throw new ScimRequestError({
detail: "User not found",
status: 404
});
if (!membership.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
return buildScimUser({
userId: membership.userId as string,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
email: membership.email,
active: true
});
};
const createScimUser = async ({ firstName, lastName, email, orgId }: TCreateScimUserDTO) => {
const org = await orgDAL.findById(orgId);
if (!org)
throw new ScimRequestError({
detail: "Organization not found",
status: 404
});
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
let user = await userDAL.findOne({
email
});
if (user) {
await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx });
if (orgMembership)
throw new ScimRequestError({
detail: "User already exists in the database",
status: 409
});
if (!orgMembership) {
await orgDAL.createMembership(
{
userId: user.id,
orgId,
inviteEmail: email,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
},
tx
);
}
});
} else {
user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create(
{
email,
firstName,
lastName,
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);
await orgDAL.createMembership(
{
inviteEmail: email,
orgId,
userId: newUser.id,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
},
tx
);
return newUser;
});
}
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.ScimUserProvisioned,
subjectLine: "Infisical organization invitation",
recipients: [email],
substitutions: {
organizationName: org.name,
callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}`
}
});
return buildScimUser({
userId: user.id,
firstName: user.firstName as string,
lastName: user.lastName as string,
email: user.email,
active: true
});
};
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
orgId
})
.catch(() => {
throw new ScimRequestError({
detail: "User not found",
status: 404
});
});
if (!membership)
throw new ScimRequestError({
detail: "User not found",
status: 404
});
if (!membership.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
let active = true;
operations.forEach((operation) => {
if (operation.op.toLowerCase() === "replace") {
if (operation.path === "active" && operation.value === "False") {
// azure scim op format
active = false;
} else if (typeof operation.value === "object" && operation.value.active === false) {
// okta scim op format
active = false;
}
}
});
if (!active) {
await deleteOrgMembership({
orgMembershipId: membership.id,
orgId: membership.orgId,
orgDAL,
projectDAL,
projectMembershipDAL
});
}
return buildScimUser({
userId: membership.userId as string,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
email: membership.email,
active
});
};
const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
orgId
})
.catch(() => {
throw new ScimRequestError({
detail: "User not found",
status: 404
});
});
if (!membership)
throw new ScimRequestError({
detail: "User not found",
status: 404
});
if (!membership.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
if (!active) {
// tx
await deleteOrgMembership({
orgMembershipId: membership.id,
orgId: membership.orgId,
orgDAL,
projectDAL,
projectMembershipDAL
});
}
return buildScimUser({
userId: membership.userId as string,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
email: membership.email,
active
});
};
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
const scimToken = await scimDAL.findById(token.scimTokenId);
if (!scimToken) throw new UnauthorizedError();
const { ttlDays, createdAt } = scimToken;
// ttl check
if (Number(ttlDays) > 0) {
const currentDate = new Date();
const scimTokenCreatedAt = new Date(createdAt);
const ttlInMilliseconds = Number(scimToken.ttlDays) * 86400 * 1000;
const expirationDate = new Date(scimTokenCreatedAt.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate)
throw new ScimRequestError({
detail: "The access token expired",
status: 401
});
}
return { scimTokenId: scimToken.id, orgId: scimToken.orgId };
};
return {
createScimToken,
listScimTokens,
deleteScimToken,
listScimUsers,
getScimUser,
createScimUser,
updateScimUser,
replaceScimUser,
fnValidateScimToken
};
};

View File

@ -0,0 +1,87 @@
import { TOrgPermission } from "@app/lib/types";
export type TCreateScimTokenDTO = {
description: string;
ttlDays: number;
} & TOrgPermission;
export type TDeleteScimTokenDTO = {
scimTokenId: string;
} & Omit<TOrgPermission, "orgId">;
// SCIM server endpoint types
export type TListScimUsersDTO = {
offset: number;
limit: number;
filter?: string;
orgId: string;
};
export type TListScimUsers = {
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
totalResults: number;
Resources: TScimUser[];
itemsPerPage: number;
startIndex: number;
};
export type TGetScimUserDTO = {
userId: string;
orgId: string;
};
export type TCreateScimUserDTO = {
email: string;
firstName: string;
lastName: string;
orgId: string;
};
export type TUpdateScimUserDTO = {
userId: string;
orgId: string;
operations: {
op: string;
path?: string;
value?:
| string
| {
active: boolean;
};
}[];
};
export type TReplaceScimUserDTO = {
userId: string;
active: boolean;
orgId: string;
};
export type TScimTokenJwtPayload = {
scimTokenId: string;
authTokenType: string;
};
export type TScimUser = {
schemas: string[];
id: string;
userName: string;
displayName: string;
name: {
givenName: string;
middleName: null;
familyName: string;
};
emails: {
primary: boolean;
value: string;
type: string;
}[];
active: boolean;
groups: string[];
meta: {
resourceType: string;
location: null;
};
};

View File

@ -1,8 +1,13 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { SecretApprovalRequestsSecretsSchema, TableName, TSecretTags } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import {
SecretApprovalRequestsSecretsSchema,
TableName,
TSecretApprovalRequestsSecrets,
TSecretTags
} from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
@ -11,6 +16,35 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => {
try {
const existingApprovalSecrets = await secretApprovalRequestSecretOrm.find(
{
$in: {
id: data.map((el) => el.id)
}
},
{ tx }
);
if (existingApprovalSecrets.length !== data.length) {
throw new BadRequestError({ message: "Some of the secret approvals do not exist" });
}
if (data.length === 0) return [];
const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret)
.insert(data)
.onConflict("id") // this will cause a conflict then merge the data
.merge() // Merge the data with the existing data
.returning("*");
return updatedApprovalSecrets;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const findByRequestId = async (requestId: string, tx?: Knex) => {
try {
const doc = await (tx || db)({
@ -190,6 +224,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
return {
...secretApprovalRequestSecretOrm,
findByRequestId,
bulkUpdateNoVersionIncrement,
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
};
};

View File

@ -11,6 +11,7 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
@ -47,6 +48,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
secretService: Pick<
TSecretServiceFactory,
| "fnSecretBulkInsert"
@ -67,6 +69,7 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequestReviewerDAL,
secretApprovalRequestSecretDAL,
secretBlindIndexDAL,
projectDAL,
permissionService,
snapshotService,
secretService,
@ -434,6 +437,8 @@ export const secretApprovalRequestServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
const folderId = folder.id;

View File

@ -8,6 +8,9 @@ import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "../config/env";
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
export type TDecryptSymmetricInput = {
ciphertext: string;
iv: string;

View File

@ -1,12 +1,20 @@
export {
buildSecretBlindIndexFromName,
createSecretBlindIndex,
decodeBase64,
decryptAsymmetric,
decryptSymmetric,
decryptSymmetric128BitHexKeyUTF8,
encodeBase64,
encryptAsymmetric,
encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8,
generateAsymmetricKeyPair
} from "./encryption";
export {
decryptIntegrationAuths,
decryptSecretApprovals,
decryptSecrets,
decryptSecretVersions
} from "./secret-encryption";
export { generateSrpServerKey, srpCheckClientProof } from "./srp";

View File

@ -0,0 +1,293 @@
import crypto from "crypto";
import { z } from "zod";
import {
IntegrationAuthsSchema,
SecretApprovalRequestsSecretsSchema,
SecretsSchema,
SecretVersionsSchema,
TIntegrationAuths,
TProjectKeys,
TSecretApprovalRequestsSecrets,
TSecrets,
TSecretVersions
} from "../../db/schemas";
import { decryptAsymmetric } from "./encryption";
const DecryptedValuesSchema = z.object({
id: z.string(),
secretKey: z.string(),
secretValue: z.string(),
secretComment: z.string().optional()
});
const DecryptedSecretSchema = z.object({
decrypted: DecryptedValuesSchema,
original: SecretsSchema
});
const DecryptedIntegrationAuthsSchema = z.object({
decrypted: z.object({
id: z.string(),
access: z.string(),
accessId: z.string(),
refresh: z.string()
}),
original: IntegrationAuthsSchema
});
const DecryptedSecretVersionsSchema = z.object({
decrypted: DecryptedValuesSchema,
original: SecretVersionsSchema
});
const DecryptedSecretApprovalsSchema = z.object({
decrypted: DecryptedValuesSchema,
original: SecretApprovalRequestsSecretsSchema
});
type DecryptedSecret = z.infer<typeof DecryptedSecretSchema>;
type DecryptedSecretVersions = z.infer<typeof DecryptedSecretVersionsSchema>;
type DecryptedSecretApprovals = z.infer<typeof DecryptedSecretApprovalsSchema>;
type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>;
type TLatestKey = TProjectKeys & {
sender: {
publicKey: string;
};
};
const decryptCipher = ({
ciphertext,
iv,
tag,
key
}: {
ciphertext: string;
iv: string;
tag: string;
key: string | Buffer;
}) => {
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64"));
decipher.setAuthTag(Buffer.from(tag, "base64"));
let cleartext = decipher.update(ciphertext, "base64", "utf8");
cleartext += decipher.final("utf8");
return cleartext;
};
const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: string }>, key: string | Buffer) => {
const results: string[] = [];
for (const { ciphertext, iv, tag } of data) {
if (!ciphertext || !iv || !tag) {
results.push("");
} else {
results.push(decryptCipher({ ciphertext, iv, tag, key }));
}
}
return results;
};
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
const key = decryptAsymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey
});
const decryptedSecrets: DecryptedSecret[] = [];
encryptedSecrets.forEach((encSecret) => {
const [secretKey, secretValue, secretComment] = getDecryptedValues(
[
{
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag
},
{
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag
},
{
ciphertext: encSecret.secretCommentCiphertext || "",
iv: encSecret.secretCommentIV || "",
tag: encSecret.secretCommentTag || ""
}
],
key
);
const decryptedSecret: DecryptedSecret = {
decrypted: {
secretKey,
secretValue,
secretComment,
id: encSecret.id
},
original: encSecret
};
decryptedSecrets.push(DecryptedSecretSchema.parse(decryptedSecret));
});
return decryptedSecrets;
};
export const decryptSecretVersions = (
encryptedSecretVersions: TSecretVersions[],
privateKey: string,
latestKey: TLatestKey
) => {
const key = decryptAsymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey
});
const decryptedSecrets: DecryptedSecretVersions[] = [];
encryptedSecretVersions.forEach((encSecret) => {
const [secretKey, secretValue, secretComment] = getDecryptedValues(
[
{
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag
},
{
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag
},
{
ciphertext: encSecret.secretCommentCiphertext || "",
iv: encSecret.secretCommentIV || "",
tag: encSecret.secretCommentTag || ""
}
],
key
);
const decryptedSecret: DecryptedSecretVersions = {
decrypted: {
secretKey,
secretValue,
secretComment,
id: encSecret.id
},
original: encSecret
};
decryptedSecrets.push(DecryptedSecretVersionsSchema.parse(decryptedSecret));
});
return decryptedSecrets;
};
export const decryptSecretApprovals = (
encryptedSecretApprovals: TSecretApprovalRequestsSecrets[],
privateKey: string,
latestKey: TLatestKey
) => {
const key = decryptAsymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey
});
const decryptedSecrets: DecryptedSecretApprovals[] = [];
encryptedSecretApprovals.forEach((encApproval) => {
const [secretKey, secretValue, secretComment] = getDecryptedValues(
[
{
ciphertext: encApproval.secretKeyCiphertext,
iv: encApproval.secretKeyIV,
tag: encApproval.secretKeyTag
},
{
ciphertext: encApproval.secretValueCiphertext,
iv: encApproval.secretValueIV,
tag: encApproval.secretValueTag
},
{
ciphertext: encApproval.secretCommentCiphertext || "",
iv: encApproval.secretCommentIV || "",
tag: encApproval.secretCommentTag || ""
}
],
key
);
const decryptedSecret: DecryptedSecretApprovals = {
decrypted: {
secretKey,
secretValue,
secretComment,
id: encApproval.id
},
original: encApproval
};
decryptedSecrets.push(DecryptedSecretApprovalsSchema.parse(decryptedSecret));
});
return decryptedSecrets;
};
export const decryptIntegrationAuths = (
encryptedIntegrationAuths: TIntegrationAuths[],
privateKey: string,
latestKey: TLatestKey
) => {
const key = decryptAsymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey
});
const decryptedIntegrationAuths: DecryptedIntegrationAuths[] = [];
encryptedIntegrationAuths.forEach((encAuth) => {
const [access, accessId, refresh] = getDecryptedValues(
[
{
ciphertext: encAuth.accessCiphertext || "",
iv: encAuth.accessIV || "",
tag: encAuth.accessTag || ""
},
{
ciphertext: encAuth.accessIdCiphertext || "",
iv: encAuth.accessIdIV || "",
tag: encAuth.accessIdTag || ""
},
{
ciphertext: encAuth.refreshCiphertext || "",
iv: encAuth.refreshIV || "",
tag: encAuth.refreshTag || ""
}
],
key
);
decryptedIntegrationAuths.push({
decrypted: {
id: encAuth.id,
access,
accessId,
refresh
},
original: encAuth
});
});
return decryptedIntegrationAuths;
};

View File

@ -1,4 +1,12 @@
import argon2 from "argon2";
import crypto from "crypto";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import tweetnacl from "tweetnacl-util";
import { TUserEncryptionKeys } from "@app/db/schemas";
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
export const generateSrpServerKey = async (salt: string, verifier: string) => {
// eslint-disable-next-line new-cap
@ -24,3 +32,99 @@ export const srpCheckClientProof = async (
server.setClientPublicKey(clientPublicKey);
return server.checkClientProof(clientProof);
};
// Ghost user related:
// This functionality is intended for ghost user logic. This happens on the frontend when a user is being created.
// We replicate the same functionality on the backend when creating a ghost user.
export const generateUserSrpKeys = async (email: string, password: string) => {
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = tweetnacl.encodeBase64(secretKeyUint8Array);
const publicKey = tweetnacl.encodeBase64(publicKeyUint8Array);
// eslint-disable-next-line
const client = new jsrp.client();
await new Promise((resolve) => {
client.init({ username: email, password }, () => resolve(null));
});
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>((resolve, reject) => {
client.createVerifier((err, res) => {
if (err) return reject(err);
return resolve(res);
});
});
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2.argon2id,
raw: true
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = encryptSymmetric(privateKey, key.toString("base64"));
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
return {
protectedKey,
plainPrivateKey: privateKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
};
};
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2.argon2id,
raw: true
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = decryptSymmetric({
ciphertext: user.protectedKey!,
iv: user.protectedKeyIV!,
tag: user.protectedKeyTag!,
key: derivedKey.toString("base64")
});
const privateKey = decryptSymmetric({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
key
});
return privateKey;
};
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
const randomBytes = crypto.randomBytes(16).toString("hex");
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
return { nonce, ciphertext };
};

View File

@ -58,3 +58,35 @@ export class BadRequestError extends Error {
this.error = error;
}
}
export class ScimRequestError extends Error {
name: string;
schemas: string[];
detail: string;
status: number;
error: unknown;
constructor({
name,
error,
detail,
status
}: {
message?: string;
name?: string;
error?: unknown;
detail: string;
status: number;
}) {
super(detail ?? "The request is invalid");
this.name = name || "ScimRequestError";
this.schemas = ["urn:ietf:params:scim:api:messages:2.0:Error"];
this.error = error;
this.detail = detail;
this.status = status;
}
}

View File

@ -1,6 +1,7 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import {
TScanFullRepoEventPayload,
@ -15,7 +16,8 @@ export enum QueueName {
IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook",
SecretFullRepoScan = "secret-full-repo-scan",
SecretPushEventScan = "secret-push-event-scan"
SecretPushEventScan = "secret-push-event-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost"
}
export enum QueueJobs {
@ -25,7 +27,8 @@ export enum QueueJobs {
AuditLogPrune = "audit-log-prune-job",
SecWebhook = "secret-webhook-trigger",
IntegrationSync = "secret-integration-pull",
SecretScan = "secret-scan"
SecretScan = "secret-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost-job"
}
export type TQueueJobTypes = {
@ -64,6 +67,20 @@ export type TQueueJobTypes = {
payload: TScanFullRepoEventPayload;
};
[QueueName.SecretPushEventScan]: { name: QueueJobs.SecretScan; payload: TScanPushEventPayload };
[QueueName.UpgradeProjectToGhost]: {
name: QueueJobs.UpgradeProjectToGhost;
payload: {
projectId: string;
startedByUserId: string;
encryptedPrivateKey: {
encryptedKey: string;
encryptedKeyIv: string;
encryptedKeyTag: string;
keyEncoding: SecretKeyEncoding;
};
};
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@ -63,6 +63,11 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
identityId: req.auth.identityId
}
};
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
payload.actor = {
type: ActorType.SCIM_CLIENT,
metadata: {}
};
} else {
throw new BadRequestError({ message: "Missing logic for other actor" });
}

View File

@ -3,6 +3,7 @@ import fp from "fastify-plugin";
import jwt, { JwtPayload } from "jsonwebtoken";
import { TServiceTokens, TUsers } from "@app/db/schemas";
import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors";
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
@ -35,6 +36,12 @@ export type TAuthMode =
actor: ActorType.IDENTITY;
identityId: string;
identityName: string;
}
| {
authMode: AuthMode.SCIM_TOKEN;
actor: ActorType.SCIM_CLIENT;
scimTokenId: string;
orgId: string;
};
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
@ -55,6 +62,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
}
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
switch (decodedToken.authTokenType) {
case AuthTokenType.ACCESS_TOKEN:
return {
@ -70,6 +78,12 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
token: decodedToken as TIdentityAccessTokenJwtPayload,
actor: ActorType.IDENTITY
} as const;
case AuthTokenType.SCIM_TOKEN:
return {
authMode: AuthMode.SCIM_TOKEN,
token: decodedToken as TScimTokenJwtPayload,
actor: ActorType.SCIM_CLIENT
} as const;
default:
return { authMode: null, token: null } as const;
}
@ -113,6 +127,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
break;
}
case AuthMode.SCIM_TOKEN: {
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId };
break;
}
default:
throw new UnauthorizedError({ name: "Unknown token strategy" });
}

View File

@ -14,6 +14,8 @@ export const injectPermission = fp(async (server) => {
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
} else if (req.auth.actor === ActorType.SERVICE) {
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId };
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId };
}
});
});

View File

@ -2,7 +2,13 @@ import { ForbiddenError } from "@casl/ability";
import fastifyPlugin from "fastify-plugin";
import { ZodError } from "zod";
import { BadRequestError, DatabaseError, InternalServerError, UnauthorizedError } from "@app/lib/errors";
import {
BadRequestError,
DatabaseError,
InternalServerError,
ScimRequestError,
UnauthorizedError
} from "@app/lib/errors";
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
server.setErrorHandler((error, req, res) => {
@ -21,6 +27,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType}`
});
} else if (error instanceof ScimRequestError) {
void res.status(error.status).send({
schemas: error.schemas,
status: error.status,
detail: error.detail
});
} else {
void res.send(error);
}

View File

@ -11,6 +11,8 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
import { scimServiceFactory } from "@app/ee/services/scim/scim-service";
import { secretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -63,6 +65,7 @@ import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service";
import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue";
import { projectServiceFactory } from "@app/services/project/project-service";
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@ -155,6 +158,7 @@ export const registerRoutes = async (
const auditLogDAL = auditLogDALFactory(db);
const trustedIpDAL = trustedIpDALFactory(db);
const scimDAL = scimDALFactory(db);
// ee db layer ops
const permissionDAL = permissionDALFactory(db);
@ -188,6 +192,7 @@ export const registerRoutes = async (
trustedIpDAL,
permissionService
});
const auditLogQueue = auditLogQueueServiceFactory({
auditLogDAL,
queueService,
@ -210,6 +215,16 @@ export const registerRoutes = async (
samlConfigDAL,
licenseService
});
const scimService = scimServiceFactory({
licenseService,
scimDAL,
userDAL,
orgDAL,
projectDAL,
projectMembershipDAL,
permissionService,
smtpService
});
const telemetryService = telemetryServiceFactory();
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
@ -266,19 +281,13 @@ export const registerRoutes = async (
secretScanningDAL,
secretScanningQueue
});
const projectService = projectServiceFactory({
permissionService,
projectDAL,
secretBlindIndexDAL,
projectEnvDAL,
projectMembershipDAL,
folderDAL,
licenseService
});
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
const projectMembershipService = projectMembershipServiceFactory({
projectMembershipDAL,
projectDAL,
permissionService,
projectBotDAL,
orgDAL,
userDAL,
smtpService,
@ -286,6 +295,46 @@ export const registerRoutes = async (
projectRoleDAL,
licenseService
});
const projectKeyService = projectKeyServiceFactory({
permissionService,
projectKeyDAL,
projectMembershipDAL
});
const projectQueueService = projectQueueFactory({
queueService,
secretDAL,
folderDAL,
projectDAL,
orgDAL,
integrationAuthDAL,
orgService,
projectEnvDAL,
userDAL,
secretVersionDAL,
projectKeyDAL,
projectBotDAL,
projectMembershipDAL,
secretApprovalRequestDAL,
secretApprovalSecretDAL: sarSecretDAL
});
const projectService = projectServiceFactory({
permissionService,
projectDAL,
projectQueue: projectQueueService,
secretBlindIndexDAL,
identityProjectDAL,
identityOrgMembershipDAL,
projectBotDAL,
projectKeyDAL,
userDAL,
projectEnvDAL,
orgService,
projectMembershipDAL,
folderDAL,
licenseService
});
const projectEnvService = projectEnvServiceFactory({
permissionService,
projectEnvDAL,
@ -293,11 +342,7 @@ export const registerRoutes = async (
projectDAL,
folderDAL
});
const projectKeyService = projectKeyServiceFactory({
permissionService,
projectKeyDAL,
projectMembershipDAL
});
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
const snapshotService = secretSnapshotServiceFactory({
@ -332,9 +377,9 @@ export const registerRoutes = async (
folderDAL,
permissionService,
secretImportDAL,
projectDAL,
secretDAL
});
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL });
const integrationAuthService = integrationAuthServiceFactory({
integrationAuthDAL,
integrationDAL,
@ -368,6 +413,7 @@ export const registerRoutes = async (
secretVersionTagDAL,
secretBlindIndexDAL,
permissionService,
projectDAL,
secretDAL,
secretTagDAL,
snapshotService,
@ -381,6 +427,7 @@ export const registerRoutes = async (
secretTagDAL,
secretApprovalRequestSecretDAL: sarSecretDAL,
secretApprovalRequestReviewerDAL: sarReviewerDAL,
projectDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretApprovalRequestDAL,
@ -486,6 +533,7 @@ export const registerRoutes = async (
secretScanning: secretScanningService,
license: licenseService,
trustedIp: trustedIpService,
scim: scimService,
secretBlindIndex: secretBlindIndexService,
telemetry: telemetryService
});

View File

@ -1,6 +1,6 @@
import { z } from "zod";
import { SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
@ -72,6 +72,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({
message: z.string(),
user: UsersSchema,
organization: OrganizationsSchema,
token: z.string(),
new: z.string()
})
@ -82,7 +83,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
const serverCfg = await getServerCfg();
if (serverCfg.initialized)
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
const { user, token } = await server.services.superAdmin.adminSignUp({
const { user, token, organization } = await server.services.superAdmin.adminSignUp({
...req.body,
ip: req.realIp,
userAgent: req.headers["user-agent"] || ""
@ -109,6 +110,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
message: "Successfully set up admin account",
user: user.user,
token: token.access,
organization,
new: "123"
};
}

View File

@ -48,6 +48,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerSecretTagRouter);
},
{ prefix: "/workspace" }
);

View File

@ -93,7 +93,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.trim()
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
.optional(),
authEnforced: z.boolean().optional()
authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional()
}),
response: {
200: z.object({

View File

@ -1,6 +1,12 @@
import { z } from "zod";
import { OrgMembershipsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import {
OrgMembershipsSchema,
ProjectMembershipRole,
ProjectMembershipsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -80,7 +86,10 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
members: req.body.members
members: req.body.members.map((member) => ({
...member,
projectRole: ProjectMembershipRole.Member
}))
});
await server.services.auditLog.createAuditLog({

View File

@ -2,13 +2,11 @@ import { z } from "zod";
import {
IntegrationsSchema,
ProjectKeysSchema,
ProjectMembershipsSchema,
ProjectsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -119,7 +117,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.getAProject({
actorId: req.permission.id,
@ -171,7 +169,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.deleteProject({
actorId: req.permission.id,
@ -216,6 +214,41 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
url: "/:workspaceId",
method: "PATCH",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
name: z.string().trim().optional(),
autoCapitalization: z.boolean().optional()
}),
response: {
200: z.object({
workspace: ProjectsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.updateProject({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
update: {
name: req.body.name,
autoCapitalization: req.body.autoCapitalization
}
});
return {
workspace
};
}
});
server.route({
url: "/:workspaceId/auto-capitalization",
method: "POST",
@ -249,48 +282,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
url: "/:workspaceId/invite-signup",
method: "POST",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
email: z.string().trim()
}),
response: {
200: z.object({
invitee: UsersSchema,
latestKey: ProjectKeysSchema.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { invitee, latestKey } = await server.services.projectMembership.inviteUserToProject({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
email: req.body.email
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.ADD_WORKSPACE_MEMBER,
metadata: {
userId: invitee.id,
email: invitee.email
}
}
});
return { invitee, latestKey };
}
});
server.route({
url: "/:workspaceId/integrations",
method: "GET",

View File

@ -2,6 +2,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router";
import { registerOrgRouter } from "./organization-router";
import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
import { registerServiceTokenRouter } from "./service-token-router";
import { registerUserRouter } from "./user-router";
@ -21,6 +22,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
async (projectServer) => {
await projectServer.register(registerProjectRouter);
await projectServer.register(registerIdentityProjectRouter);
await projectServer.register(registerProjectMembershipRouter);
},
{ prefix: "/workspace" }
);

View File

@ -0,0 +1,95 @@
import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/memberships",
schema: {
params: z.object({
projectId: z.string().describe("The ID of the project.")
}),
body: z.object({
emails: z.string().email().array().describe("Emails of the users to add to the project.")
}),
response: {
200: z.object({
memberships: ProjectMembershipsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({
projectId: req.params.projectId,
actorId: req.permission.id,
actor: req.permission.type,
emails: req.body.emails
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
metadata: memberships.map(({ userId, id }) => ({
userId: userId || "",
membershipId: id,
email: ""
}))
}
});
return { memberships };
}
});
server.route({
method: "DELETE",
url: "/:projectId/memberships",
schema: {
params: z.object({
projectId: z.string().describe("The ID of the project.")
}),
body: z.object({
emails: z.string().email().array().describe("Emails of the users to remove from the project.")
}),
response: {
200: z.object({
memberships: ProjectMembershipsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const memberships = await server.services.projectMembership.deleteProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
emails: req.body.emails
});
for (const membership of memberships) {
// eslint-disable-next-line no-await-in-loop
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.projectId,
event: {
type: EventType.REMOVE_WORKSPACE_MEMBER,
metadata: {
userId: membership.userId,
email: ""
}
}
});
}
return { memberships };
}
});
};

View File

@ -1,11 +1,21 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectKeysSchema } from "@app/db/schemas";
import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const projectWithEnv = ProjectsSchema.merge(
z.object({
_id: z.string(),
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
})
);
export const registerProjectRouter = async (server: FastifyZodProvider) => {
/* Get project key */
server.route({
url: "/:workspaceId/encrypted-key",
method: "GET",
@ -34,8 +44,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const key = await server.services.projectKey.getLatestProjectKey({
actor: req.permission.type,
actorId: req.permission.id,
projectId: req.params.workspaceId,
actorOrgId: req.permission.orgId
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
@ -52,4 +62,97 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return key;
}
});
/* Start upgrade of a project */
server.route({
url: "/:projectId/upgrade",
method: "POST",
schema: {
params: z.object({
projectId: z.string().trim()
}),
body: z.object({
userPrivateKey: z.string().trim()
}),
response: {
200: z.void()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
await server.services.project.upgradeProject({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.projectId,
userPrivateKey: req.body.userPrivateKey
});
}
});
/* Get upgrade status of project */
server.route({
url: "/:projectId/upgrade/status",
method: "GET",
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
status: z.string().nullable()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const status = await server.services.project.getProjectUpgradeStatus({
projectId: req.params.projectId,
actor: req.permission.type,
actorId: req.permission.id
});
return { status };
}
});
/* Create new project */
server.route({
method: "POST",
url: "/",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
projectName: z.string().trim(),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional(),
organizationId: z.string().trim()
}),
response: {
200: z.object({
project: projectWithEnv
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const project = await server.services.project.createProject({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.body.organizationId,
workspaceName: req.body.projectName,
slug: req.body.slug
});
return { project };
}
});
};

View File

@ -275,7 +275,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
if (!user) {
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] });
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod], isGhost: false });
}
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
const isUserCompleted = user.isAccepted;

View File

@ -50,7 +50,7 @@ export const authSignupServiceFactory = ({
throw new Error("Failed to send verification code for complete account");
}
if (!user) {
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email });
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email, isGhost: false });
}
if (!user) throw new Error("Failed to create user");

View File

@ -17,21 +17,24 @@ export enum AuthTokenType {
API_KEY = "apiKey",
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
SCIM_TOKEN = "scimToken"
}
export enum AuthMode {
JWT = "jwt",
SERVICE_TOKEN = "serviceToken",
API_KEY = "apiKey",
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
SCIM_TOKEN = "scimToken"
}
export enum ActorType { // would extend to AWS, Azure, ...
USER = "user", // userIdentity
SERVICE = "service",
IDENTITY = "identity",
Machine = "machine"
Machine = "machine",
SCIM_CLIENT = "scimClient"
}
export type AuthModeJwtTokenPayload = {

View File

@ -1,10 +1,35 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TableName, TIntegrationAuths, TIntegrationAuthsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TIntegrationAuthDALFactory = ReturnType<typeof integrationAuthDALFactory>;
export const integrationAuthDALFactory = (db: TDbClient) => {
const integrationAuthOrm = ormify(db, TableName.IntegrationAuth);
return integrationAuthOrm;
const bulkUpdate = async (
data: Array<{ filter: Partial<TIntegrationAuths>; data: TIntegrationAuthsUpdate }>,
tx?: Knex
) => {
try {
const integrationAuths = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.IntegrationAuth).where(filter).update(updateData).returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return integrationAuths;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
return {
...integrationAuthOrm,
bulkUpdate
};
};

View File

@ -76,7 +76,8 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
);
)
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, firstName, lastName, id: userId, publicKey }
@ -86,6 +87,79 @@ export const orgDALFactory = (db: TDbClient) => {
}
};
const findOrgMembersByEmail = async (orgId: string, emails: string[]) => {
try {
const members = await db(TableName.OrgMembership)
.where({ orgId })
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("email").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("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ isGhost: false })
.whereIn("email", emails);
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all org members" });
}
};
const findOrgGhostUser = async (orgId: string) => {
try {
const member = await db(TableName.OrgMembership)
.where({ orgId })
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ isGhost: true })
.first();
return member;
} catch (error) {
return null;
}
};
const ghostUserExists = async (orgId: string) => {
try {
const member = await db(TableName.OrgMembership)
.where({ orgId })
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
.select(db.ref("id").withSchema(TableName.Users).as("userId"))
.where({ isGhost: true })
.first();
return Boolean(member);
} catch (error) {
return false;
}
};
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
try {
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
@ -165,7 +239,14 @@ export const orgDALFactory = (db: TDbClient) => {
// eslint-disable-next-line
.where(buildFindFilter(filter))
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.select(selectAllTableCols(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users));
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization)
);
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
@ -184,6 +265,9 @@ export const orgDALFactory = (db: TDbClient) => {
findAllOrgMembers,
findOrgById,
findAllOrgsByUserId,
ghostUserExists,
findOrgMembersByEmail,
findOrgGhostUser,
create,
updateById,
deleteById,

View File

@ -0,0 +1,41 @@
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
type TDeleteOrgMembership = {
orgMembershipId: string;
orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
projectDAL: Pick<TProjectDALFactory, "find">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
};
export const deleteOrgMembership = async ({
orgMembershipId,
orgId,
orgDAL,
projectDAL,
projectMembershipDAL
}: TDeleteOrgMembership) => {
const membership = await orgDAL.transaction(async (tx) => {
// delete org membership
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
const projects = await projectDAL.find({ orgId }, { tx });
// delete associated project memberships
await projectMembershipDAL.delete(
{
$in: {
projectId: projects.map((project) => project.id)
},
userId: orgMembership.userId as string
},
tx
);
return orgMembership;
});
return membership;
};

View File

@ -1,6 +1,8 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import crypto from "crypto";
import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
@ -11,6 +13,7 @@ import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-
import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
@ -28,6 +31,7 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
import {
TDeleteOrgMembershipDTO,
TFindAllWorkspacesDTO,
TFindOrgMembersByEmailDTO,
TInviteUserToOrgDTO,
TUpdateOrgDTO,
TUpdateOrgMembershipDTO,
@ -93,6 +97,15 @@ export const orgServiceFactory = ({
return members;
};
const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
const members = await orgDAL.findOrgMembersByEmail(orgId, emails);
return members;
};
const findAllWorkspaces = async ({ actor, actorId, actorOrgId, orgId }: TFindAllWorkspacesDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
@ -118,6 +131,54 @@ export const orgServiceFactory = ({
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
};
const addGhostUser = async (orgId: string, tx?: Knex) => {
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
const password = crypto.randomBytes(128).toString("hex");
const user = await userDAL.create(
{
isGhost: true,
authMethods: [AuthMethod.EMAIL],
email,
isAccepted: true
},
tx
);
const encKeys = await generateUserSrpKeys(email, password);
await userDAL.upsertUserEncryptionKey(
user.id,
{
encryptionVersion: 2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier
},
tx
);
const createMembershipData = {
orgId,
userId: user.id,
role: OrgMembershipRole.Admin,
status: OrgMembershipStatus.Accepted
};
await orgDAL.createMembership(createMembershipData, tx);
return {
user,
keys: encKeys
};
};
/*
* Update organization details
* */
@ -126,16 +187,32 @@ export const orgServiceFactory = ({
actorId,
actorOrgId,
orgId,
data: { name, slug, authEnforced }
data: { name, slug, authEnforced, scimEnabled }
}: TUpdateOrgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
const plan = await licenseService.getPlan(orgId);
if (authEnforced !== undefined) {
if (!plan?.samlSSO)
throw new BadRequestError({
message:
"Failed to enforce/un-enforce SAML SSO due to plan restriction. Upgrade plan to enforce/un-enforce SAML SSO."
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
}
if (authEnforced) {
if (scimEnabled !== undefined) {
if (!plan?.scim)
throw new BadRequestError({
message:
"Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning."
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
}
if (authEnforced || scimEnabled) {
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
if (!samlCfg)
throw new BadRequestError({
@ -147,7 +224,8 @@ export const orgServiceFactory = ({
const org = await orgDAL.updateById(orgId, {
name,
slug: slug ? slugify(slug) : undefined,
authEnforced
authEnforced,
scimEnabled
});
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
return org;
@ -321,7 +399,8 @@ export const orgServiceFactory = ({
{
email: inviteeEmail,
isAccepted: false,
authMethods: [AuthMethod.EMAIL]
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);
@ -470,10 +549,12 @@ export const orgServiceFactory = ({
inviteUserToOrganization,
verifyUserToOrg,
updateOrg,
findOrgMembersByEmail,
createOrganization,
deleteOrganizationById,
deleteOrgMembership,
findAllWorkspaces,
addGhostUser,
updateOrgMembership,
// incident contacts
findIncidentContacts,

View File

@ -30,6 +30,13 @@ export type TVerifyUserToOrgDTO = {
code: string;
};
export type TFindOrgMembersByEmailDTO = {
actor: ActorType;
actorId: string;
orgId: string;
emails: string[];
};
export type TFindAllWorkspacesDTO = {
actor: ActorType;
actorId: string;
@ -38,5 +45,5 @@ export type TFindAllWorkspacesDTO = {
};
export type TUpdateOrgDTO = {
data: Partial<{ name: string; slug: string; authEnforced: boolean }>;
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
} & TOrgPermission;

View File

@ -27,5 +27,19 @@ export const projectBotDALFactory = (db: TDbClient) => {
}
};
return { ...projectBotOrm, findOne };
const findProjectByBotId = async (botId: string) => {
try {
const project = await db(TableName.ProjectBot)
.where({ [`${TableName.ProjectBot}.id` as "id"]: botId })
.join(TableName.Project, `${TableName.ProjectBot}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.Project))
.first();
return project || null;
} catch (error) {
throw new DatabaseError({ error, name: "Find project by bot id" });
}
};
return { ...projectBotOrm, findOne, findProjectByBotId };
};

View File

@ -1,125 +1,111 @@
import { ForbiddenError } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import {
decryptAsymmetric,
decryptSymmetric,
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8,
generateAsymmetricKeyPair
} from "@app/lib/crypto";
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "./project-bot-dal";
import { TSetActiveStateDTO } from "./project-bot-types";
import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types";
type TProjectBotServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findById">;
projectBotDAL: TProjectBotDALFactory;
};
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => {
const getBotKey = async (projectId: string) => {
const appCfg = getConfig();
const encryptionKey = appCfg.ENCRYPTION_KEY;
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
export const projectBotServiceFactory = ({
projectBotDAL,
projectDAL,
permissionService
}: TProjectBotServiceFactoryDep) => {
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const getBotKey = async (projectId: string) => {
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
throw new BadRequestError({ message: "Encryption key missing" });
if (rootEncryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.BASE64) {
const privateKeyBot = decryptSymmetric({
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey,
key: rootEncryptionKey
});
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: privateKeyBot,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
}
if (encryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.UTF8) {
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey,
key: encryptionKey
});
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: privateKeyBot,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
}
const botPrivateKey = getBotPrivateKey({ bot });
throw new BadRequestError({
message: "Failed to obtain bot copy of workspace key needed for operation"
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
};
const findBotByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TProjectPermission) => {
const findBotByProjectId = async ({
actorId,
actor,
projectId,
actorOrgId,
privateKey,
botKey,
publicKey
}: TFindBotByProjectIdDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const appCfg = getConfig();
const bot = await projectBotDAL.transaction(async (tx) => {
const doc = await projectBotDAL.findOne({ projectId }, tx);
if (doc) return doc;
const { publicKey, privateKey } = generateAsymmetricKeyPair();
if (appCfg.ROOT_ENCRYPTION_KEY) {
const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY);
return projectBotDAL.create(
{
name: "Infisical Bot",
projectId,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: false,
publicKey,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.BASE64
},
tx
);
const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair();
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(keys.privateKey);
const project = await projectDAL.findById(projectId, tx);
if (project.version === ProjectVersion.V2) {
throw new BadRequestError({ message: "Failed to create bot, project is upgraded." });
}
if (appCfg.ENCRYPTION_KEY) {
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(privateKey, appCfg.ENCRYPTION_KEY);
return projectBotDAL.create(
{
name: "Infisical Bot",
projectId,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: false,
publicKey,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
},
tx
);
}
throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" });
return projectBotDAL.create(
{
name: "Infisical Bot",
projectId,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: false,
publicKey: keys.publicKey,
algorithm,
keyEncoding: encoding,
...(botKey && {
encryptedProjectKey: botKey.encryptedKey,
encryptedProjectKeyNonce: botKey.nonce
})
},
tx
);
});
return bot;
};
const findProjectByBotId = async (botId: string) => {
try {
const bot = await projectBotDAL.findProjectByBotId(botId);
return bot;
} catch (e) {
throw new BadRequestError({ message: "Failed to find bot by ID" });
}
};
const setBotActiveState = async ({ actor, botId, botKey, actorId, actorOrgId, isActive }: TSetActiveStateDTO) => {
const bot = await projectBotDAL.findById(botId);
if (!bot) throw new BadRequestError({ message: "Bot not found" });
@ -127,6 +113,16 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
const { permission } = await permissionService.getProjectPermission(actor, actorId, bot.projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
const project = await projectBotDAL.findProjectByBotId(botId);
if (!project) {
throw new BadRequestError({ message: "Failed to find project by bot ID" });
}
if (project.version === ProjectVersion.V2) {
throw new BadRequestError({ message: "Failed to set bot active for upgraded project. Bot is already active" });
}
if (isActive) {
if (!botKey?.nonce || !botKey?.encryptedKey) {
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
@ -153,6 +149,8 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
return {
findBotByProjectId,
setBotActiveState,
getBotPrivateKey,
findProjectByBotId,
getBotKey
};
};

View File

@ -1,3 +1,4 @@
import { TProjectBots } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
export type TSetActiveStateDTO = {
@ -8,3 +9,16 @@ export type TSetActiveStateDTO = {
};
botId: string;
} & Omit<TProjectPermission, "projectId">;
export type TFindBotByProjectIdDTO = {
privateKey?: string;
publicKey?: string;
botKey?: {
nonce: string;
encryptedKey: string;
};
} & TProjectPermission;
export type TGetPrivateKeyDTO = {
bot: TProjectBots;
};

View File

@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TProjectKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
@ -10,10 +12,11 @@ export const projectKeyDALFactory = (db: TDbClient) => {
const findLatestProjectKey = async (
userId: string,
projectId: string
projectId: string,
tx?: Knex
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
try {
const projectKey = await db(TableName.ProjectKeys)
const projectKey = await (tx || db)(TableName.ProjectKeys)
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
.where({ projectId, receiverId: userId })
@ -29,9 +32,9 @@ export const projectKeyDALFactory = (db: TDbClient) => {
}
};
const findAllProjectUserPubKeys = async (projectId: string) => {
const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => {
try {
const pubKeys = await db(TableName.ProjectMembership)
const pubKeys = await (tx || db)(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)

View File

@ -1,7 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
@ -24,20 +24,63 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
db.ref("projectId").withSchema(TableName.ProjectMembership),
db.ref("role").withSchema(TableName.ProjectMembership),
db.ref("roleId").withSchema(TableName.ProjectMembership),
db.ref("isGhost").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId")
);
return members.map(({ email, firstName, lastName, publicKey, ...data }) => ({
)
.where({ isGhost: false });
return members.map(({ email, firstName, lastName, publicKey, isGhost, ...data }) => ({
...data,
user: { email, firstName, lastName, id: data.userId, publicKey }
user: { email, firstName, lastName, id: data.userId, publicKey, isGhost }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all project members" });
}
};
return { ...projectMemberOrm, findAllProjectMembers };
const findProjectGhostUser = async (projectId: string) => {
try {
const ghostUser = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))
.where({ isGhost: true })
.first();
return ghostUser;
} catch (error) {
throw new DatabaseError({ error, name: "Find project top-level user" });
}
};
const findMembershipsByEmail = async (projectId: string, emails: string[]) => {
try {
const members = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.select(
selectAllTableCols(TableName.ProjectMembership),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("email").withSchema(TableName.Users)
)
.whereIn("email", emails)
.where({ isGhost: false });
return members.map(({ userId, email, ...data }) => ({
...data,
user: { id: userId, email }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find members by email" });
}
};
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByEmail };
};

View File

@ -1,15 +1,28 @@
/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
import {
OrgMembershipStatus,
ProjectMembershipRole,
ProjectVersion,
SecretKeyEncoding,
TableName,
TProjectMemberships,
TUsers
} 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 { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ActorType } from "../auth/auth-type";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
@ -17,7 +30,9 @@ import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
import {
TAddUsersToWorkspaceDTO,
TDeleteProjectMembershipDTO,
TAddUsersToWorkspaceNonE2EEDTO,
TDeleteProjectMembershipOldDTO,
TDeleteProjectMembershipsDTO,
TGetProjectMembershipDTO,
TInviteUserToProjectDTO,
TUpdateProjectMembershipDTO
@ -26,11 +41,12 @@ import {
type TProjectMembershipServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: TSmtpService;
projectBotDAL: TProjectBotDALFactory;
projectMembershipDAL: TProjectMembershipDALFactory;
userDAL: Pick<TUserDALFactory, "findById" | "findOne">;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
projectDAL: Pick<TProjectDALFactory, "findById">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByEmail">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@ -42,6 +58,7 @@ export const projectMembershipServiceFactory = ({
projectMembershipDAL,
smtpService,
projectRoleDAL,
projectBotDAL,
orgDAL,
userDAL,
projectDAL,
@ -55,64 +72,90 @@ export const projectMembershipServiceFactory = ({
return projectMembershipDAL.findAllProjectMembers(projectId);
};
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, email }: TInviteUserToProjectDTO) => {
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, emails }: TInviteUserToProjectDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const invitee = await userDAL.findOne({ email });
if (!invitee || !invitee.isAccepted)
throw new BadRequestError({
message: "Faield to validate invitee",
name: "Invite user to project"
});
const inviteeMembership = await projectMembershipDAL.findOne({
userId: invitee.id,
projectId
});
if (inviteeMembership)
throw new BadRequestError({
message: "Existing member of project",
name: "Invite user to project"
});
const invitees: TUsers[] = [];
const project = await projectDAL.findById(projectId);
const inviteeMembershipOrg = await orgDAL.findMembership({
userId: invitee.id,
orgId: project.orgId,
status: OrgMembershipStatus.Accepted
const users = await userDAL.find({
$in: { email: emails }
});
if (!inviteeMembershipOrg)
throw new BadRequestError({
message: "Failed to validate invitee org membership",
name: "Invite user to project"
await projectDAL.transaction(async (tx) => {
for (const invitee of users) {
if (!invitee.isAccepted)
throw new BadRequestError({
message: "Failed to validate invitee",
name: "Invite user to project"
});
const inviteeMembership = await projectMembershipDAL.findOne(
{
userId: invitee.id,
projectId
},
tx
);
if (inviteeMembership) {
throw new BadRequestError({
message: "Existing member of project",
name: "Invite user to project"
});
}
const inviteeMembershipOrg = await orgDAL.findMembership({
userId: invitee.id,
orgId: project.orgId,
status: OrgMembershipStatus.Accepted
});
if (!inviteeMembershipOrg) {
throw new BadRequestError({
message: "Failed to validate invitee org membership",
name: "Invite user to project"
});
}
await projectMembershipDAL.create(
{
userId: invitee.id,
projectId,
role: ProjectMembershipRole.Member
},
tx
);
invitees.push(invitee);
}
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: invitees.map((i) => i.email),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
});
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
await projectMembershipDAL.create({
userId: invitee.id,
projectId,
role: ProjectMembershipRole.Member
});
const sender = await userDAL.findById(actorId);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: [invitee.email],
substitutions: {
inviterFirstName: sender.firstName,
inviterEmail: sender.email,
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
return { invitee, latestKey };
return { invitees, latestKey };
};
const addUsersToProject = async ({ projectId, actorId, actor, actorOrgId, members }: TAddUsersToWorkspaceDTO) => {
const addUsersToProject = async ({
projectId,
actorId,
actor,
actorOrgId,
members,
sendEmails = true
}: TAddUsersToWorkspaceDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
@ -134,11 +177,16 @@ export const projectMembershipServiceFactory = ({
await projectMembershipDAL.transaction(async (tx) => {
await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({
projectId,
userId: userId as string,
role: ProjectMembershipRole.Member
})),
orgMembers.map(({ userId, id: membershipId }) => {
const role =
members.find((i) => i.orgMembershipId === membershipId)?.projectRole || ProjectMembershipRole.Member;
return {
projectId,
userId: userId as string,
role
};
}),
tx
);
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
@ -153,22 +201,132 @@ export const projectMembershipServiceFactory = ({
tx
);
});
const sender = await userDAL.findById(actorId);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
substitutions: {
inviterFirstName: sender.firstName,
inviterEmail: sender.email,
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
if (sendEmails) {
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
return orgMembers;
};
const addUsersToProjectNonE2EE = async ({
projectId,
actorId,
actor,
emails,
sendEmails = true
}: TAddUsersToWorkspaceNonE2EEDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (project.version === ProjectVersion.V1) {
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
}
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails);
if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" });
const existingMembers = await projectMembershipDAL.find({
projectId,
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const newWsMembers = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: orgMembers.map((membership) => ({
orgMembershipId: membership.id,
projectMembershipRole: ProjectMembershipRole.Member,
userPublicKey: membership.user.publicKey
}))
});
const members: TProjectMemberships[] = [];
await projectMembershipDAL.transaction(async (tx) => {
const result = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id,
role: ProjectMembershipRole.Member
})),
tx
);
members.push(...result);
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
tx
);
});
if (sendEmails) {
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: orgMembers.map(({ user }) => user.email).filter(Boolean),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
return members;
};
const updateProjectMembership = async ({
actorId,
actor,
@ -180,6 +338,15 @@ export const projectMembershipServiceFactory = ({
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
if (membershipUser?.isGhost) {
throw new BadRequestError({
message: "Unauthorized member update",
name: "Update project membership"
});
}
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
if (isCustomRole) {
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
@ -205,16 +372,26 @@ export const projectMembershipServiceFactory = ({
return membership;
};
// This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now.
const deleteProjectMembership = async ({
actorId,
actor,
actorOrgId,
projectId,
membershipId
}: TDeleteProjectMembershipDTO) => {
}: TDeleteProjectMembershipOldDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
const member = await userDAL.findUserByProjectMembershipId(membershipId);
if (member?.isGhost) {
throw new BadRequestError({
message: "Unauthorized member delete",
name: "Delete project membership"
});
}
const membership = await projectMembershipDAL.transaction(async (tx) => {
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
@ -223,11 +400,78 @@ export const projectMembershipServiceFactory = ({
return membership;
};
const deleteProjectMemberships = async ({
actorId,
actor,
actorOrgId,
projectId,
emails
}: TDeleteProjectMembershipsDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
const project = await projectDAL.findById(projectId);
if (!project) {
throw new BadRequestError({
message: "Project not found",
name: "Delete project membership"
});
}
if (project.version === ProjectVersion.V1) {
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
}
const projectMembers = await projectMembershipDAL.findMembershipsByEmail(projectId, emails);
if (projectMembers.length !== emails.length) {
throw new BadRequestError({
message: "Some users are not part of project",
name: "Delete project membership"
});
}
if (actor === ActorType.USER && projectMembers.some(({ user }) => user.id === actorId)) {
throw new BadRequestError({
message: "Cannot remove yourself from project",
name: "Delete project membership"
});
}
const memberships = await projectMembershipDAL.transaction(async (tx) => {
const deletedMemberships = await projectMembershipDAL.delete(
{
projectId,
$in: {
id: projectMembers.map(({ id }) => id)
}
},
tx
);
await projectKeyDAL.delete(
{
projectId,
$in: {
receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean)
}
},
tx
);
return deletedMemberships;
});
return memberships;
};
return {
getProjectMemberships,
inviteUserToProject,
updateProjectMembership,
deleteProjectMembership,
addUsersToProjectNonE2EE,
deleteProjectMemberships,
deleteProjectMembership, // TODO: Remove this
addUsersToProject
};
};

View File

@ -1,9 +1,10 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
export type TGetProjectMembershipDTO = TProjectPermission;
export type TInviteUserToProjectDTO = {
email: string;
emails: string[];
} & TProjectPermission;
export type TUpdateProjectMembershipDTO = {
@ -11,14 +12,25 @@ export type TUpdateProjectMembershipDTO = {
role: string;
} & TProjectPermission;
export type TDeleteProjectMembershipDTO = {
export type TDeleteProjectMembershipOldDTO = {
membershipId: string;
} & TProjectPermission;
export type TDeleteProjectMembershipsDTO = {
emails: string[];
} & TProjectPermission;
export type TAddUsersToWorkspaceDTO = {
sendEmails?: boolean;
members: {
orgMembershipId: string;
workspaceEncryptedKey: string;
workspaceEncryptedNonce: string;
projectRole: ProjectMembershipRole;
}[];
} & TProjectPermission;
export type TAddUsersToWorkspaceNonE2EEDTO = {
sendEmails?: boolean;
emails: string[];
} & TProjectPermission;

View File

@ -1,6 +1,8 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { ProjectsSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
@ -52,6 +54,32 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const findProjectGhostUser = async (projectId: string) => {
try {
const ghostUser = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))
.where({ isGhost: true })
.first();
return ghostUser;
} catch (error) {
throw new DatabaseError({ error, name: "Find project top-level user" });
}
};
const setProjectUpgradeStatus = async (projectId: string, status: ProjectUpgradeStatus | null, tx?: Knex) => {
try {
const data: TProjectsUpdate = {
upgradeStatus: status
} as const;
await (tx || db)(TableName.Project).where({ id: projectId }).update(data);
} catch (error) {
throw new DatabaseError({ error, name: "Set project upgrade status" });
}
};
const findAllProjectsByIdentity = async (identityId: string) => {
try {
const workspaces = await db(TableName.IdentityProjectMembership)
@ -132,10 +160,25 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const checkProjectUpgradeStatus = async (projectId: string) => {
const project = await projectOrm.findById(projectId);
const upgradeInProgress =
project.upgradeStatus === ProjectUpgradeStatus.InProgress && project.version === ProjectVersion.V1;
if (upgradeInProgress) {
throw new BadRequestError({
message: "Project is currently being upgraded, and secrets cannot be written. Please try again"
});
}
};
return {
...projectOrm,
findAllProjects,
setProjectUpgradeStatus,
findAllProjectsByIdentity,
findProjectById
findProjectGhostUser,
findProjectById,
checkProjectUpgradeStatus
};
};

View File

@ -0,0 +1,50 @@
import crypto from "crypto";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { AddUserToWsDTO } from "./project-types";
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
const plaintextProjectKey = decryptAsymmetric({
ciphertext: decryptKey.encryptedKey,
nonce: decryptKey.nonce,
publicKey: decryptKey.sender.publicKey,
privateKey: userPrivateKey
});
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => {
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
plaintextProjectKey,
userPublicKey,
userPrivateKey
);
return {
orgMembershipId,
projectRole: projectMembershipRole,
workspaceEncryptedKey: inviteeCipherText,
workspaceEncryptedNonce: inviteeNonce
};
});
return newWsMembers;
};
type TCreateProjectKeyDTO = {
publicKey: string;
privateKey: string;
};
export const createProjectKey = ({ publicKey, privateKey }: TCreateProjectKeyDTO) => {
// 3. Create a random key that we'll use as the project key.
const randomBytes = crypto.randomBytes(16).toString("hex");
// 4. Encrypt the project key with the users key pair.
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
randomBytes,
publicKey,
privateKey
);
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
};

View File

@ -0,0 +1,541 @@
/* eslint-disable no-await-in-loop */
import {
IntegrationAuthsSchema,
ProjectMembershipRole,
ProjectUpgradeStatus,
ProjectVersion,
SecretApprovalRequestsSecretsSchema,
SecretKeyEncoding,
SecretsSchema,
SecretVersionsSchema,
TIntegrationAuths,
TSecretApprovalRequestsSecrets,
TSecrets,
TSecretVersions
} from "@app/db/schemas";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import {
decryptIntegrationAuths,
decryptSecretApprovals,
decryptSecrets,
decryptSecretVersions
} from "@app/lib/crypto";
import {
decryptAsymmetric,
encryptSymmetric128BitHexKeyUTF8,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
export type TProjectQueueFactory = ReturnType<typeof projectQueueFactory>;
type TProjectQueueFactoryDep = {
queueService: TQueueServiceFactory;
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "bulkUpdateNoVersionIncrement" | "delete">;
folderDAL: Pick<TSecretFolderDALFactory, "find">;
secretDAL: Pick<TSecretDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "find" | "create" | "delete" | "insertMany">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "find">;
secretApprovalSecretDAL: Pick<TSecretApprovalRequestSecretDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "delete" | "create">;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
integrationAuthDAL: TIntegrationAuthDALFactory;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "transaction" | "updateById" | "setProjectUpgradeStatus" | "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
};
export const projectQueueFactory = ({
queueService,
secretDAL,
folderDAL,
userDAL,
secretVersionDAL,
integrationAuthDAL,
secretApprovalRequestDAL,
secretApprovalSecretDAL,
projectKeyDAL,
projectBotDAL,
projectEnvDAL,
orgDAL,
projectDAL,
orgService,
projectMembershipDAL
}: TProjectQueueFactoryDep) => {
const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => {
await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, {
attempts: 1,
removeOnComplete: true,
removeOnFail: {
count: 5 // keep the most recent jobs
}
});
};
queueService.start(QueueName.UpgradeProjectToGhost, async ({ data }) => {
try {
const [project] = await projectDAL.find({
id: data.projectId,
version: ProjectVersion.V1
});
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId);
if (!project || !oldProjectKey) {
throw new Error("Project or project key not found");
}
if (project.upgradeStatus !== ProjectUpgradeStatus.Failed && project.upgradeStatus !== null) {
throw new Error("Project upgrade status is not valid");
}
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.InProgress); // Set the status to in progress. This is important to prevent multiple upgrades at the same time.
// eslint-disable-next-line no-promise-executor-return
// await new Promise((resolve) => setTimeout(resolve, 50_000));
const userPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: data.encryptedPrivateKey.keyEncoding,
ciphertext: data.encryptedPrivateKey.encryptedKey,
iv: data.encryptedPrivateKey.encryptedKeyIv,
tag: data.encryptedPrivateKey.encryptedKeyTag
});
const projectEnvs = await projectEnvDAL.find({
projectId: project.id
});
const projectFolders = await folderDAL.find({
$in: {
envId: projectEnvs.map((env) => env.id)
}
});
// Get all the secrets within the project (as encrypted)
const projectIntegrationAuths = await integrationAuthDAL.find({
projectId: project.id
});
const secrets: TSecrets[] = [];
const secretVersions: TSecretVersions[] = [];
const approvalSecrets: TSecretApprovalRequestsSecrets[] = [];
const folderSecretVersionIdsToDelete: string[] = [];
for (const folder of projectFolders) {
const folderSecrets = await secretDAL.find({ folderId: folder.id });
const folderSecretVersions = await secretVersionDAL.find(
{
folderId: folder.id
},
// Only get the latest 700 secret versions for each folder.
{
limit: 1000,
sort: [["createdAt", "desc"]]
}
);
const deletedSecretVersions = await secretVersionDAL.find(
{
folderId: folder.id
},
{
// Get all the secret versions that are not the latest 700
offset: 1000
}
);
folderSecretVersionIdsToDelete.push(...deletedSecretVersions.map((el) => el.id));
const approvalRequests = await secretApprovalRequestDAL.find({
status: RequestState.Open,
folderId: folder.id
});
const secretApprovals = await secretApprovalSecretDAL.find({
$in: {
requestId: approvalRequests.map((el) => el.id)
}
});
secrets.push(...folderSecrets);
secretVersions.push(...folderSecretVersions);
approvalSecrets.push(...secretApprovals);
}
const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
const decryptedSecretVersions = decryptSecretVersions(secretVersions, userPrivateKey, oldProjectKey);
const decryptedApprovalSecrets = decryptSecretApprovals(approvalSecrets, userPrivateKey, oldProjectKey);
const decryptedIntegrationAuths = decryptIntegrationAuths(projectIntegrationAuths, userPrivateKey, oldProjectKey);
// Get the existing bot and the existing project keys for the members of the project
const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
// TRANSACTION START
await projectDAL.transaction(async (tx) => {
await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
// Create a ghost user
const ghostUser = await orgService.addGhostUser(project.orgId, tx);
// Create a project key
const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
publicKey: ghostUser.keys.publicKey,
privateKey: ghostUser.keys.plainPrivateKey
});
// Create a new project key for the GHOST
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: ghostUser.user.id,
encryptedKey: newEncryptedProjectKey,
nonce: newEncryptedProjectKeyIv,
senderId: ghostUser.user.id
},
tx
);
// Create a membership for the ghost user
await projectMembershipDAL.create(
{
projectId: project.id,
userId: ghostUser.user.id,
role: ProjectMembershipRole.Admin
},
tx
);
// If a bot already exists, delete it
if (existingBot) {
await projectBotDAL.delete({ id: existingBot.id }, tx);
}
// Delete all the existing project keys
await projectKeyDAL.delete(
{
projectId: project.id,
$in: {
id: existingProjectKeys.map((key) => key.id)
}
},
tx
);
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
if (!ghostUserLatestKey) {
throw new Error("User latest key not found (V2 Upgrade)");
}
const newProjectMembers: {
encryptedKey: string;
nonce: string;
senderId: string;
receiverId: string;
projectId: string;
}[] = [];
for (const key of existingProjectKeys) {
const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
if (!user || !orgMembership) {
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
}
const [newMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id,
projectMembershipRole: ProjectMembershipRole.Admin
}
]
});
newProjectMembers.push({
encryptedKey: newMember.workspaceEncryptedKey,
nonce: newMember.workspaceEncryptedNonce,
senderId: ghostUser.user.id,
receiverId: user.id,
projectId: project.id
});
}
// Create project keys for all the old members
await projectKeyDAL.insertMany(newProjectMembers, tx);
// Encrypt the bot private key (which is the same as the ghost user)
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
// 5. Create a bot for the project
const newBot = await projectBotDAL.create(
{
name: "Infisical Bot (Ghost)",
projectId: project.id,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
senderId: ghostUser.user.id,
encryptedProjectKey: newEncryptedProjectKey,
encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
algorithm,
keyEncoding: encoding
},
tx
);
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
iv: newBot.iv,
tag: newBot.tag,
ciphertext: newBot.encryptedPrivateKey
});
const botKey = decryptAsymmetric({
ciphertext: newBot.encryptedProjectKey!,
privateKey: botPrivateKey,
nonce: newBot.encryptedProjectKeyNonce!,
publicKey: ghostUser.keys.publicKey
});
const updatedSecrets: TSecrets[] = [];
const updatedSecretVersions: TSecretVersions[] = [];
const updatedSecretApprovals: TSecretApprovalRequestsSecrets[] = [];
const updatedIntegrationAuths: TIntegrationAuths[] = [];
for (const rawSecret of decryptedSecrets) {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretKey, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
rawSecret.decrypted.secretComment || "",
botKey
);
const payload: TSecrets = {
...rawSecret.original,
keyEncoding: SecretKeyEncoding.UTF8,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
} as const;
if (!SecretsSchema.safeParse(payload).success) {
throw new Error(`Invalid secret payload: ${JSON.stringify(payload)}`);
}
updatedSecrets.push(payload);
}
for (const rawSecretVersion of decryptedSecretVersions) {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretVersion.decrypted.secretKey, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(
rawSecretVersion.decrypted.secretValue || "",
botKey
);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
rawSecretVersion.decrypted.secretComment || "",
botKey
);
const payload: TSecretVersions = {
...rawSecretVersion.original,
keyEncoding: SecretKeyEncoding.UTF8,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
} as const;
if (!SecretVersionsSchema.safeParse(payload).success) {
throw new Error(`Invalid secret version payload: ${JSON.stringify(payload)}`);
}
updatedSecretVersions.push(payload);
}
for (const rawSecretApproval of decryptedApprovalSecrets) {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretApproval.decrypted.secretKey, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(
rawSecretApproval.decrypted.secretValue || "",
botKey
);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
rawSecretApproval.decrypted.secretComment || "",
botKey
);
const payload: TSecretApprovalRequestsSecrets = {
...rawSecretApproval.original,
keyEncoding: SecretKeyEncoding.UTF8,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
} as const;
if (!SecretApprovalRequestsSecretsSchema.safeParse(payload).success) {
throw new Error(`Invalid secret approval payload: ${JSON.stringify(payload)}`);
}
updatedSecretApprovals.push(payload);
}
for (const integrationAuth of decryptedIntegrationAuths) {
const access = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.access, botKey);
const accessId = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.accessId, botKey);
const refresh = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.refresh, botKey);
const payload: TIntegrationAuths = {
...integrationAuth.original,
keyEncoding: SecretKeyEncoding.UTF8,
accessCiphertext: access.ciphertext,
accessIV: access.iv,
accessTag: access.tag,
accessIdCiphertext: accessId.ciphertext,
accessIdIV: accessId.iv,
accessIdTag: accessId.tag,
refreshCiphertext: refresh.ciphertext,
refreshIV: refresh.iv,
refreshTag: refresh.tag
} as const;
if (!IntegrationAuthsSchema.safeParse(payload).success) {
throw new Error(`Invalid integration auth payload: ${JSON.stringify(payload)}`);
}
updatedIntegrationAuths.push(payload);
}
if (updatedSecrets.length !== secrets.length) {
throw new Error("Failed to update some secrets");
}
if (updatedSecretVersions.length !== secretVersions.length) {
throw new Error("Failed to update some secret versions");
}
if (updatedSecretApprovals.length !== approvalSecrets.length) {
throw new Error("Failed to update some secret approvals");
}
if (updatedIntegrationAuths.length !== projectIntegrationAuths.length) {
throw new Error("Failed to update some integration auths");
}
const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(updatedSecrets, tx);
const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(updatedSecretVersions, tx);
const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
updatedSecretApprovals,
tx
);
const integrationAuthUpdates = await integrationAuthDAL.bulkUpdate(
updatedIntegrationAuths.map((el) => ({
filter: { id: el.id },
data: {
...el,
id: undefined
}
})),
tx
);
// Delete all secret versions that are no longer needed. We only store the latest 100 versions for each secret.
await secretVersionDAL.delete(
{
$in: {
id: folderSecretVersionIdsToDelete
}
},
tx
);
if (
secretUpdates.length !== updatedSecrets.length ||
secretVersionUpdates.length !== updatedSecretVersions.length ||
secretApprovalUpdates.length !== updatedSecretApprovals.length ||
integrationAuthUpdates.length !== updatedIntegrationAuths.length
) {
throw new Error("Parts of the upgrade failed. Some secrets were not updated");
}
await projectDAL.setProjectUpgradeStatus(data.projectId, null, tx);
// await new Promise((resolve) => setTimeout(resolve, 15_000));
// throw new Error("Transaction was successful!");
});
} catch (err) {
const [project] = await projectDAL
.find({
id: data.projectId,
version: ProjectVersion.V1
})
.catch(() => [null]);
if (!project) {
logger.error("Failed to upgrade project, because no project was found", data);
} else {
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
logger.error(err, "Failed to upgrade project");
}
throw err;
}
});
queueService.listen(QueueName.UpgradeProjectToGhost, "failed", (job, err) => {
logger.error(err, "Upgrade project failed", job?.data);
});
return {
upgradeProject
};
};

View File

@ -1,22 +1,40 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { ProjectMembershipRole } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal";
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
import { TProjectQueueFactory } from "./project-queue";
import {
TCreateProjectDTO,
TDeleteProjectDTO,
TGetProjectDTO,
TUpdateProjectDTO,
TUpgradeProjectDTO
} from "./project-types";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
@ -26,11 +44,18 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
projectQueue: TProjectQueueFactory;
userDAL: TUserDALFactory;
folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@ -38,8 +63,15 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
export const projectServiceFactory = ({
projectDAL,
projectQueue,
projectKeyDAL,
permissionService,
userDAL,
folderDAL,
orgService,
identityProjectDAL,
projectBotDAL,
identityOrgMembershipDAL,
secretBlindIndexDAL,
projectMembershipDAL,
projectEnvDAL,
@ -48,8 +80,13 @@ export const projectServiceFactory = ({
/*
* Create workspace. Make user the admin
* */
const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName }: TCreateProjectDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName, slug }: TCreateProjectDTO) => {
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
actor,
actorId,
orgId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
const appCfg = getConfig();
@ -64,20 +101,28 @@ export const projectServiceFactory = ({
});
}
const newProject = projectDAL.transaction(async (tx) => {
const results = await projectDAL.transaction(async (tx) => {
const ghostUser = await orgService.addGhostUser(orgId, tx);
const project = await projectDAL.create(
{ name: workspaceName, orgId, slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`) },
{
name: workspaceName,
orgId,
slug: slug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
version: ProjectVersion.V2
},
tx
);
// set user as admin member for proeject
// set ghost user as admin of project
await projectMembershipDAL.create(
{
userId: actorId,
userId: ghostUser.user.id,
role: ProjectMembershipRole.Admin,
projectId: project.id
},
tx
);
// generate the blind index for project
await secretBlindIndexDAL.create(
{
@ -99,18 +144,165 @@ export const projectServiceFactory = ({
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
tx
);
// _id for backward compat
return { ...project, environments: envs, _id: project.id };
// 3. Create a random key that we'll use as the project key.
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: ghostUser.keys.publicKey,
privateKey: ghostUser.keys.plainPrivateKey
});
// 4. Save the project key for the ghost user.
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: ghostUser.user.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: ghostUser.user.id
},
tx
);
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
// 5. Create & a bot for the project
await projectBotDAL.create(
{
name: "Infisical Bot (Ghost)",
projectId: project.id,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
senderId: ghostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
// Find the ghost users latest key
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
if (!latestKey) {
throw new Error("Latest key not found for user");
}
// If the project is being created by a user, add the user to the project as an admin
if (actor === ActorType.USER) {
// Find public key of user
const user = await userDAL.findUserEncKeyByUserId(actorId);
if (!user) {
throw new Error("User not found");
}
const [projectAdmin] = assignWorkspaceKeysToMembers({
decryptKey: latestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id,
projectMembershipRole: ProjectMembershipRole.Admin
}
]
});
// Create a membership for the user
await projectMembershipDAL.create(
{
projectId: project.id,
userId: user.id,
role: projectAdmin.projectRole
},
tx
);
// Create a project key for the user
await projectKeyDAL.create(
{
encryptedKey: projectAdmin.workspaceEncryptedKey,
nonce: projectAdmin.workspaceEncryptedNonce,
senderId: ghostUser.user.id,
receiverId: user.id,
projectId: project.id
},
tx
);
}
// If the project is being created by an identity, add the identity to the project as an admin
else if (actor === ActorType.IDENTITY) {
// Find identity org membership
const identityOrgMembership = await identityOrgMembershipDAL.findOne(
{
identityId: actorId,
orgId: project.orgId
},
tx
);
// If identity org membership not found, throw error
if (!identityOrgMembership) {
throw new BadRequestError({
message: `Failed to find identity with id ${actorId}`
});
}
// Get the role permission for the identity
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
ProjectMembershipRole.Admin,
orgId
);
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const isCustomRole = Boolean(customRole);
await identityProjectDAL.create(
{
identityId: actorId,
projectId: project.id,
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
roleId: customRole?.id
},
tx
);
}
return {
...project,
environments: envs,
_id: project.id
};
});
return newProject;
return results;
};
const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
const deletedProject = await projectDAL.deleteById(projectId);
const deletedProject = await projectDAL.transaction(async (tx) => {
const project = await projectDAL.deleteById(projectId, tx);
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(projectId).catch(() => null);
// Delete the org membership for the ghost user if it's found.
if (projectGhostUser) {
await userDAL.deleteById(projectGhostUser.id, tx);
}
return project;
});
return deletedProject;
};
@ -124,6 +316,17 @@ export const projectServiceFactory = ({
return projectDAL.findProjectById(projectId);
};
const updateProject = async ({ projectId, actor, actorId, actorOrgId, update }: TUpdateProjectDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, {
name: update.name,
autoCapitalization: update.autoCapitalization
});
return updatedProject;
};
const toggleAutoCapitalization = async ({
projectId,
actor,
@ -146,12 +349,55 @@ export const projectServiceFactory = ({
return updatedProject;
};
const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
if (membership?.role !== ProjectMembershipRole.Admin) {
throw new ForbiddenRequestError({
message: "User must be admin"
});
}
const encryptedPrivateKey = infisicalSymmetricEncypt(userPrivateKey);
await projectQueue.upgradeProject({
projectId,
startedByUserId: actorId,
encryptedPrivateKey: {
encryptedKey: encryptedPrivateKey.ciphertext,
encryptedKeyIv: encryptedPrivateKey.iv,
encryptedKeyTag: encryptedPrivateKey.tag,
keyEncoding: encryptedPrivateKey.encoding
}
});
};
const getProjectUpgradeStatus = async ({ projectId, actor, actorId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
const project = await projectDAL.findProjectById(projectId);
if (!project) {
throw new BadRequestError({
message: `Project with id ${projectId} not found`
});
}
return project.upgradeStatus || null;
};
return {
createProject,
deleteProject,
getProjects,
updateProject,
getProjectUpgradeStatus,
getAProject,
toggleAutoCapitalization,
updateName
updateName,
upgradeProject
};
};

View File

@ -1,3 +1,6 @@
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
export type TCreateProjectDTO = {
@ -6,6 +9,7 @@ export type TCreateProjectDTO = {
actorOrgId?: string;
orgId: string;
workspaceName: string;
slug?: string;
};
export type TDeleteProjectDTO = {
@ -21,3 +25,24 @@ export type TGetProjectDTO = {
actorOrgId?: string;
projectId: string;
};
export type TUpdateProjectDTO = {
update: {
name?: string;
autoCapitalization?: boolean;
};
} & TProjectPermission;
export type TUpgradeProjectDTO = {
userPrivateKey: string;
} & TProjectPermission;
export type AddUserToWsDTO = {
decryptKey: TProjectKeys & { sender: { publicKey: string } };
userPrivateKey: string;
members: {
orgMembershipId: string;
projectMembershipRole: ProjectMembershipRole;
userPublicKey: string;
}[];
};

View File

@ -4,6 +4,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
@ -21,6 +22,7 @@ type TSecretImportServiceFactoryDep = {
secretImportDAL: TSecretImportDALFactory;
folderDAL: TSecretFolderDALFactory;
secretDAL: Pick<TSecretDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
projectEnvDAL: TProjectEnvDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@ -34,6 +36,7 @@ export const secretImportServiceFactory = ({
projectEnvDAL,
permissionService,
folderDAL,
projectDAL,
secretDAL
}: TSecretImportServiceFactoryDep) => {
const createImport = async ({
@ -62,6 +65,8 @@ export const secretImportServiceFactory = ({
})
);
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });

View File

@ -22,7 +22,11 @@ export const secretDALFactory = (db: TDbClient) => {
// the idea is to use postgres specific function
// insert with id this will cause a conflict then merge the data
const bulkUpdate = async (data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>, tx?: Knex) => {
const bulkUpdate = async (
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
@ -41,6 +45,35 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
const bulkUpdateNoVersionIncrement = async (data: TSecrets[], tx?: Knex) => {
try {
const existingSecrets = await secretOrm.find(
{
$in: {
id: data.map((el) => el.id)
}
},
{ tx }
);
if (existingSecrets.length !== data.length) {
throw new BadRequestError({ message: "Some of the secrets do not exist" });
}
if (data.length === 0) return [];
const updatedSecrets = await (tx || db)(TableName.Secret)
.insert(data)
.onConflict("id") // this will cause a conflict then merge the data
.merge() // Merge the data with the existing data
.returning("*");
return updatedSecrets;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const deleteMany = async (
data: Array<{ blindIndex: string; type: SecretType }>,
folderId: string,
@ -145,5 +178,13 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes };
return {
...secretOrm,
update,
bulkUpdate,
deleteMany,
bulkUpdateNoVersionIncrement,
findByFolderId,
findByBlindIndexes
};
};

View File

@ -11,6 +11,7 @@ import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
@ -49,6 +50,7 @@ type TSecretServiceFactoryDep = {
secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@ -68,6 +70,7 @@ export const secretServiceFactory = ({
permissionService,
snapshotService,
secretQueueService,
projectDAL,
projectBotService,
secretImportDAL,
secretVersionTagDAL
@ -281,6 +284,8 @@ export const secretServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
@ -359,6 +364,8 @@ export const secretServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
@ -459,6 +466,8 @@ export const secretServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
@ -650,6 +659,8 @@ export const secretServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
@ -705,6 +716,8 @@ export const secretServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
@ -776,6 +789,8 @@ export const secretServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;

View File

@ -1,8 +1,8 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretVersions } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
@ -36,6 +36,57 @@ export const secretVersionDALFactory = (db: TDbClient) => {
}
};
const bulkUpdate = async (
data: Array<{ filter: Partial<TSecretVersions>; data: TSecretVersionsUpdate }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.SecretVersion)
.where(filter)
.update(updateData)
.increment("version", 1) // TODO: Is this really needed?
.returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const bulkUpdateNoVersionIncrement = async (data: TSecretVersions[], tx?: Knex) => {
try {
const existingSecretVersions = await secretVersionOrm.find(
{
$in: {
id: data.map((el) => el.id)
}
},
{ tx }
);
if (existingSecretVersions.length !== data.length) {
throw new BadRequestError({ message: "Some of the secret versions do not exist" });
}
if (data.length === 0) return [];
const updatedSecretVersions = await (tx || db)(TableName.SecretVersion)
.insert(data)
.onConflict("id") // this will cause a conflict then merge the data
.merge() // Merge the data with the existing data
.returning("*");
return updatedSecretVersions;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
try {
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(TableName.SecretVersion)
@ -59,5 +110,11 @@ export const secretVersionDALFactory = (db: TDbClient) => {
}
};
return { ...secretVersionOrm, findLatestVersionMany, findLatestVersionByFolderId };
return {
...secretVersionOrm,
findLatestVersionMany,
bulkUpdate,
findLatestVersionByFolderId,
bulkUpdateNoVersionIncrement
};
};

View File

@ -25,7 +25,8 @@ export enum SmtpTemplates {
OrgInvite = "organizationInvitation.handlebars",
ResetPassword = "passwordReset.handlebars",
SecretLeakIncident = "secretLeakIncident.handlebars",
WorkspaceInvite = "workspaceInvitation.handlebars"
WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars"
}
export enum SmtpHost {

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Organization Invitation</title>
</head>
<body>
<h2>Join your organization on Infisical</h2>
<p>You've been invited to join the Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>
</html>

View File

@ -1,15 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Project Invitation</title>
</head>
<body>
</head>
<body>
<h2>Join your team on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
<p>You have been invited to a new Infisical project — {{workspaceName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
and configs.</p>
</body>
</html>

View File

@ -70,6 +70,7 @@ export const superAdminServiceFactory = ({
lastName,
email,
superAdmin: true,
isGhost: false,
isAccepted: true,
authMethods: [AuthMethod.EMAIL]
},
@ -96,7 +97,11 @@ export const superAdminServiceFactory = ({
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, initialOrganizationName);
const organization = await orgService.createOrganization(
userInfo.user.id,
userInfo.user.email,
initialOrganizationName
);
await updateServerCfg({ initialized: true });
const token = await authService.generateUserTokens({
@ -106,7 +111,7 @@ export const superAdminServiceFactory = ({
organizationId: undefined
});
// TODO(akhilmhdh-pg): telemetry service
return { token, user: userInfo };
return { token, user: userInfo, organization };
};
return {

View File

@ -23,7 +23,7 @@ export const userDALFactory = (db: TDbClient) => {
const findUserEncKeyByEmail = async (email: string) => {
try {
return await db(TableName.Users)
.where({ email })
.where({ email, isGhost: false })
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
.first();
} catch (error) {
@ -47,6 +47,17 @@ export const userDALFactory = (db: TDbClient) => {
}
};
const findUserByProjectMembershipId = async (projectMembershipId: string) => {
try {
return await db(TableName.ProjectMembership)
.where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.first();
} catch (error) {
throw new DatabaseError({ error, name: "Find user by project membership id" });
}
};
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
try {
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
@ -111,6 +122,7 @@ export const userDALFactory = (db: TDbClient) => {
findUserEncKeyByEmail,
findUserEncKeyByUserId,
updateUserEncryptionByUserId,
findUserByProjectMembershipId,
upsertUserEncryptionKey,
createUserEncryption,
findOneUserAction,

View File

@ -0,0 +1,4 @@
---
title: "Create Workspace"
openapi: "POST /api/v2/workspace"
---

View File

@ -0,0 +1,8 @@
---
title: "Delete Workspace"
openapi: "DELETE /api/v1/workspace/{workspaceId}"
---
<Warning>
This operation is irreversible. All data associated with the project will be deleted. Please use with caution.
</Warning>

View File

@ -0,0 +1,4 @@
---
title: "Get Workspace"
openapi: "GET /api/v1/workspace/{workspaceId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Invite Member"
openapi: "POST /api/v2/workspace/{projectId}/memberships"
---

View File

@ -0,0 +1,4 @@
---
title: "Remove Member"
openapi: "DELETE /api/v2/workspace/{projectId}/memberships"
---

View File

@ -0,0 +1,4 @@
---
title: "Update Workspace"
openapi: "PATCH /api/v1/workspace/{workspaceId}"
---

View File

@ -0,0 +1,74 @@
---
title: "Azure SCIM"
description: "Configure SCIM provisioning with Azure for Infisical"
---
<Info>
Azure SCIM provisioning is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact team@infisical.com to purchase an enterprise license to use it.
</Info>
Prerequisites:
- [Configure Azure SAML for Infisical](/documentation/platform/sso/azure)
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow Azure to provision/deprovision users for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)
Next, press **Manage SCIM Tokens** and then **Create** to generate a SCIM token for Azure.
![SCIM create token](/images/platform/scim/scim-create-token.png)
Next, copy the **SCIM URL** and **New SCIM Token** to use when configuring SCIM in Azure.
![SCIM copy token](/images/platform/scim/scim-copy-token.png)
</Step>
<Step title="Configure SCIM in Azure">
In Azure, head to your Enterprise Application > Provisioning > Overview and press **Get started**.
![SCIM Azure](/images/platform/scim/azure/scim-azure-get-started.png)
Next, set the following fields:
- Provisioning Mode: Select **Automatic**.
- Tenant URL: Input **SCIM URL** from Step 1.
- Secret Token: Input the **New SCIM Token** from Step 1.
Afterwards, press the **Test Connection** button to check that SCIM is configured properly.
![SCIM Azure](/images/platform/scim/azure/scim-azure-config.png)
After you hit **Save**, select **Provision Microsoft Entra ID Users** under the **Mappings** subsection.
![SCIM Azure](/images/platform/scim/azure/scim-azure-select-user-mappings.png)
Next, adjust the mappings so you have them configured as below:
![SCIM Azure](/images/platform/scim/azure/scim-azure-user-mappings.png)
Finally, head to your Enterprise Application > Provisioning and set the **Provisioning Status** to **On**.
![SCIM Azure](/images/platform/scim/azure/scim-azure-provisioning-status.png)
Alternatively, you can go to **Overview** and press **Start provisioning** to have Azure start provisioning/deprovisioning users to Infisical.
![SCIM Azure](/images/platform/scim/azure/scim-azure-start-provisioning.png)
Now Azure can provision/deprovision users to/from your organization in Infisical.
</Step>
</Steps>
**FAQ**
<AccordionGroup>
<Accordion title="Why do SCIM-provisioned users have to finish setting up their account?">
Infisical's SCIM implmentation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform.
For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets.
</Accordion>
</AccordionGroup>

View File

@ -0,0 +1,64 @@
---
title: "JumpCloud SCIM"
description: "Configure SCIM provisioning with JumpCloud for Infisical"
---
<Info>
JumpCloud SCIM provisioning is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact team@infisical.com to purchase an enterprise license to use it.
</Info>
Prerequisites:
- [Configure JumpCloud SAML for Infisical](/documentation/platform/sso/jumpcloud)
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)
Next, press **Manage SCIM Tokens** and then **Create** to generate a SCIM token for JumpCloud.
![SCIM create token](/images/platform/scim/scim-create-token.png)
Next, copy the **SCIM URL** and **New SCIM Token** to use when configuring SCIM in JumpCloud.
![SCIM copy token](/images/platform/scim/scim-copy-token.png)
</Step>
<Step title="Configure SCIM in JumpCloud">
In JumpCloud, head to your Application > Identity Management > Configuration settings and make sure that
**API Type** is set to **SCIM API** and **SCIM Version** is set to **SCIM 2.0**.
![SCIM JumpCloud](/images/platform/scim/jumpcloud/scim-jumpcloud-api-type.png)
Next, set the following SCIM connection fields:
- Base URL: Input the **SCIM URL** from Step 1.
- Token Key: Input the **New SCIM Token** from Step 1.
- Test User Email: Input a test user email to be used by JumpCloud for testing the SCIM connection.
Alos, under HTTP Header > Authorization: Bearer, input the **New SCIM Token** from Step 1.
![SCIM JumpCloud](/images/platform/scim/jumpcloud/scim-jumpcloud-config.png)
Next, press **Test Connection** to check that SCIM is configured properly. Finally, press **Activate**
to have JumpCloud start provisioning/deprovisioning users to Infisical.
![SCIM JumpCloud](/images/platform/scim/jumpcloud/scim-jumpcloud-test-connection.png)
Now JumpCloud can provision/deprovision users to/from your organization in Infisical.
</Step>
</Steps>
**FAQ**
<AccordionGroup>
<Accordion title="Why do SCIM-provisioned users have to finish setting up their account?">
Infisical's SCIM implmentation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform.
For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets.
</Accordion>
</AccordionGroup>

View File

@ -0,0 +1,70 @@
---
title: "Okta SCIM"
description: "Configure SCIM provisioning with Okta for Infisical"
---
<Info>
Okta SCIM provisioning is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact team@infisical.com to purchase an enterprise license to use it.
</Info>
Prerequisites:
- [Configure Okta SAML for Infisical](/documentation/platform/sso/okta)
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)
Next, press **Manage SCIM Tokens** and then **Create** to generate a SCIM token for Okta.
![SCIM create token](/images/platform/scim/scim-create-token.png)
Next, copy the **SCIM URL** and **New SCIM Token** to use when configuring SCIM in Okta.
![SCIM copy token](/images/platform/scim/scim-copy-token.png)
</Step>
<Step title="Configure SCIM in Okta">
In Okta, head to your Application > General > App Settings. Next, select **Edit** and check the box
labled **Enable SCIM provisioning**.
![SCIM Okta](/images/platform/scim/okta/scim-okta-enable-provisioning.png)
Next, head to Provisioning > Integration and set the following SCIM connection fields:
- SCIM connector base URL: Input the **SCIM URL** from Step 1.
- Unique identifier field for users: Input `email`.
- Supported provisioning actions: Select **Push New Users** and **Push Profile Updates**.
- Authentication Mode: `HTTP Header`.
![SCIM Okta](/images/platform/scim/okta/scim-okta-config.png)
Under HTTP Header > Authorization: Bearer, input the **New SCIM Token** from Step 1.
![SCIM Okta](/images/platform/scim/okta/scim-okta-auth.png)
Next, press **Test Connector Configuration** to check that SCIM is configured properly.
![SCIM Okta](/images/platform/scim/okta/scim-okta-test.png)
Next, head to Provisioning > To App and check the boxes labeled **Enable** for **Create Users**, **Update User Attributes**, and **Deactivate Users**.
![SCIM Okta](/images/platform/scim/okta/scim-okta-app-settings.png)
Now Okta can provision/deprovision users to/from your organization in Infisical.
</Step>
</Steps>
**FAQ**
<AccordionGroup>
<Accordion title="Why do SCIM-provisioned users have to finish setting up their account?">
Infisical's SCIM implmentation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform.
For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets.
</Accordion>
</AccordionGroup>

View File

@ -0,0 +1,32 @@
---
title: "SCIM Overview"
description: "Provision users for Infisical via SCIM"
---
<Info>
SCIM provisioning is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact team@infisical.com to purchase an enterprise license to use it.
</Info>
You can configure your organization in Infisical to have members be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc.
- Provisioning: The SCIM provider pushes user information to Infisical. If the user exists in Infisical, Infisical sends an email invitation to add them to the relevant organization in Infisical; if not, Infisical initializes a new user and sends them an email invitation to finish setting up their account in the organization.
- Deprovisioning: The SCIM provider instructs Infisical to remove user(s) from an organization in Infisical.
SCIM providers:
- [Okta SCIM](/documentation/platform/scim/okta)
- [Azure SCIM](/documentation/platform/scim/azure)
- [JumpCloud SCIM](/documentation/platform/scim/jumpcloud)
**FAQ**
<AccordionGroup>
<Accordion title="Why do SCIM-provisioned users have to finish setting up their account?">
Infisical's SCIM implementation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform.
For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets.
</Accordion>
</AccordionGroup>

View File

@ -12,7 +12,7 @@ description: "Configure Azure SAML for Infisical SSO"
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
Next, copy the **Reply URL (Assertion Consumer Service URL)** and **Identifier (Entity ID)** to use when configuring the Azure SAML application.
@ -101,6 +101,11 @@ description: "Configure Azure SAML for Infisical SSO"
To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Azure user with Infisical;
Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
<Warning>
We recommend ensuring that your account is provisioned the application in Azure
prior to enforcing SAML SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps>

View File

@ -12,7 +12,7 @@ description: "Configure JumpCloud SAML for Infisical SSO"
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
Next, copy the **ACS URL** and **SP Entity ID** to use when configuring the JumpCloud SAML application.
@ -81,6 +81,11 @@ description: "Configure JumpCloud SAML for Infisical SSO"
To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one JumpCloud user with Infisical;
Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
<Warning>
We recommend ensuring that your account is provisioned the application in JumpCloud
prior to enforcing SAML SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps>

View File

@ -12,7 +12,7 @@ description: "Configure Okta SAML 2.0 for Infisical SSO"
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
Next, copy the **Single sign-on URL** and **Audience URI (SP Entity ID)** to use when configuring the Okta SAML 2.0 application.
![Okta SAML initial configuration](../../../images/sso/okta/init-config.png)
@ -84,6 +84,11 @@ description: "Configure Okta SAML 2.0 for Infisical SSO"
To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Okta user with Infisical;
Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
<Warning>
We recommend ensuring that your account is provisioned the application in Okta
prior to enforcing SAML SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps>

View File

@ -3,13 +3,13 @@ title: "SSO Overview"
description: "Log in to Infisical via SSO protocols"
---
<Warning>
<Info>
Infisical offers Google SSO and GitHub SSO for free across both Infisical Cloud and Infisical Self-hosted.
Infisical also offers SAML SSO authentication but as paid features that can be unlocked on Infisical Cloud's **Pro** tier
or via enterprise license on self-hosted instances of Infisical. On this front, we support industry-leading providers including
Okta, Azure AD, and JumpCloud; with any questions, please reach out to [sales@infisical.com](mailto:sales@infisical.com).
</Warning>
Okta, Azure AD, and JumpCloud; with any questions, please reach out to team@infisical.com.
</Info>
You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

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