Compare commits

..

395 Commits

Author SHA1 Message Date
46f0fb7a41 add helm with postgres + docs 2024-02-23 21:20:58 -05:00
2142f5736c Merge pull request #1454 from Infisical/snyk-upgrade-adb30694539efb19ffe3e73890bb6b8e
[Snyk] Upgrade aws-sdk from 2.1545.0 to 2.1549.0
2024-02-23 13:39:50 -05:00
ce764d70ad Merge branch 'main' into snyk-upgrade-adb30694539efb19ffe3e73890bb6b8e 2024-02-23 13:39:43 -05:00
c2d0ddb2fc Merge pull request #1455 from Infisical/snyk-upgrade-d2de299aecc7f7e0bc3f4806027b63db
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.502.0 to 3.504.0
2024-02-23 13:38:39 -05:00
7ba9588509 Merge pull request #1456 from Infisical/snyk-upgrade-321e83ad66200d3a9986ba215332d07e
[Snyk] Upgrade axios from 1.6.4 to 1.6.7
2024-02-23 13:38:24 -05:00
cddb09e031 Merge pull request #1457 from Infisical/snyk-upgrade-ef9af10ff1fb4ccb502ef71e8db6e32a
[Snyk] Upgrade dotenv from 16.3.1 to 16.4.1
2024-02-23 13:38:13 -05:00
046dc83638 Merge pull request #1458 from Infisical/snyk-upgrade-79bce0f7942398d32958cc208d2bf3d9
[Snyk] Upgrade @fastify/cors from 8.4.1 to 8.5.0
2024-02-23 13:38:00 -05:00
320074ef6c fix: upgrade @fastify/cors from 8.4.1 to 8.5.0
Snyk has created this PR to upgrade @fastify/cors from 8.4.1 to 8.5.0.

See this package in npm:
https://www.npmjs.com/package/@fastify/cors

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-23 17:54:12 +00:00
e780ee6573 fix: upgrade dotenv from 16.3.1 to 16.4.1
Snyk has created this PR to upgrade dotenv from 16.3.1 to 16.4.1.

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

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-23 17:54:08 +00:00
a5a881c382 fix: upgrade axios from 1.6.4 to 1.6.7
Snyk has created this PR to upgrade axios from 1.6.4 to 1.6.7.

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

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-23 17:54:04 +00:00
200d4a5af6 fix: upgrade @aws-sdk/client-secrets-manager from 3.502.0 to 3.504.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.502.0 to 3.504.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-23 17:54:00 +00:00
07318ec54b fix: upgrade aws-sdk from 2.1545.0 to 2.1549.0
Snyk has created this PR to upgrade aws-sdk from 2.1545.0 to 2.1549.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

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-23 17:53:57 +00:00
92d237a714 Merge pull request #1452 from Infisical/snyk-fix-619f8d103918c8185c0381a3771a208f
[Snyk] Security upgrade sanitize-html from 2.11.0 to 2.12.1
2024-02-23 09:49:03 -05:00
6ef988fa86 Merge pull request #1444 from akhilmhdh/feat/telemetry-new-fields
feat(telemetry): added new telemetry events
2024-02-23 09:48:42 -05:00
70822d0d98 fix: frontend/package.json & frontend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-SANITIZEHTML-6256334
2024-02-23 11:28:36 +00:00
e91499b301 feat(telemetry): updated telemetry distinct id to use service token from secrets one 2024-02-23 13:22:53 +05:30
92acb4d943 feat(telemetry): fixed typo in filename lib/telemetry and capitalized new posthog event names 2024-02-23 13:19:16 +05:30
76daa20d69 feat(telemetry): added telemetry events for creation of integration,project,machine identity and user invitation to org 2024-02-23 13:19:16 +05:30
a231813f01 Merge pull request #1445 from Nithishvb/fix-uppercase-member-search 2024-02-23 01:02:01 -05:00
3eb2bdb191 Merge pull request #1447 from Infisical/daniel/show-upgrade-to-everyone
(style): Show upgrade alert to all users
2024-02-22 22:31:05 -05:00
cadf6e1157 Update UpgradeOverlay.tsx 2024-02-23 01:13:26 +01:00
ceb7fafc06 Update UpgradeProjectAlert.tsx 2024-02-23 01:12:22 +01:00
3063bb9982 Update UpgradeProjectAlert.tsx 2024-02-23 01:12:02 +01:00
3d82a43615 Merge pull request #1446 from Infisical/daniel/ghost-users-fixes
(Fix): Project upgrade
2024-02-22 17:20:44 -05:00
028541a18a Update project-membership-service.ts 2024-02-22 23:18:32 +01:00
66a631ff46 Update SecretOverviewPage.tsx 2024-02-22 23:16:21 +01:00
28adb8f0ac Re-add upgrade popup 2024-02-22 23:12:41 +01:00
5c988c2cd5 Docs 2024-02-22 23:07:07 +01:00
acf8a54abb Fix service tokens not working after upgrade 2024-02-22 23:06:55 +01:00
387094aa27 Allow deleting project members even on V1 projects 2024-02-22 23:06:24 +01:00
4251e95c15 Merge pull request #1435 from Salman2301/fix-typo-doc-port
docs(sdk): fix typo PORT
2024-02-22 16:26:23 -05:00
f4386c2d93 Merge pull request #1443 from akhilmhdh/fix/service-token-email-telemetry
fix(telemetry): added back email for telemetry when using service token
2024-02-22 16:23:00 -05:00
ff4b943854 fix admin signup phrasing 2024-02-22 16:04:12 -05:00
879a5ecfac Merge pull request #1389 from Salman2301/feat-admin-signup
Add admin invite only signup field
2024-02-22 15:39:06 -05:00
a831a7d848 small typos and update migration timestamp 2024-02-22 15:35:09 -05:00
3138784d1a Merge branch 'main' into feat-admin-signup 2024-02-22 15:13:28 -05:00
0b258e3918 make clear service token with null creator 2024-02-22 15:06:00 -05:00
d0545a01b9 Merge pull request #1436 from Infisical/snyk-upgrade-12de5d06bf0c091610dcb9c1c21ef687
[Snyk] Upgrade mysql2 from 3.6.5 to 3.9.1
2024-02-22 14:46:59 -05:00
d71398344f Merge pull request #1437 from Infisical/snyk-upgrade-28f9cfd389af9a9d16a7b1f08b87f2b4
[Snyk] Upgrade bullmq from 5.1.1 to 5.1.6
2024-02-22 14:46:46 -05:00
25e3cc047b Merge pull request #1438 from Infisical/snyk-upgrade-69ae80b8287feeb813ee50b31af31ab0
[Snyk] Upgrade zod-to-json-schema from 3.22.0 to 3.22.4
2024-02-22 14:46:36 -05:00
17149de567 Merge pull request #1439 from Infisical/snyk-upgrade-a58302f2598bcda51b89dcf7293bd0ad
[Snyk] Upgrade @fastify/swagger from 8.12.0 to 8.14.0
2024-02-22 14:46:23 -05:00
cca2fb7ff5 Merge pull request #1440 from Infisical/snyk-upgrade-d48144dd89e80fb18b02fe45b4dfd7c8
[Snyk] Upgrade fastify from 4.24.3 to 4.26.0
2024-02-22 14:46:12 -05:00
f1f2d62993 fix: access member returns empty results when search using upper case letters. 2024-02-22 22:32:58 +05:30
be49de5f34 fix(telemetry): added back email for telemetry when using service token 2024-02-22 17:09:33 +05:30
acfa89ba8b fix typo 2024-02-22 05:01:50 -05:00
389ec85554 Merge pull request #1442 from akhilmhdh/ci/frontend-ts-check
chore: added type and lint check for frontend ci/cd
2024-02-22 04:54:23 -05:00
2a6b0efe22 Merge pull request #1441 from Infisical/update-self-host-config-docker-compose
Update self host config docker compose
2024-02-22 04:45:59 -05:00
74d9a76f75 update self config/docker guide 2024-02-22 04:42:39 -05:00
9c67d43ebe remove upgrade popup 2024-02-22 03:27:41 -05:00
d8f3531b50 chore: added type and lint check for frontend 2024-02-22 13:28:07 +05:30
57be73c17e fix: upgrade fastify from 4.24.3 to 4.26.0
Snyk has created this PR to upgrade fastify from 4.24.3 to 4.26.0.

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

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-22 07:47:03 +00:00
a10129e750 fix: upgrade @fastify/swagger from 8.12.0 to 8.14.0
Snyk has created this PR to upgrade @fastify/swagger from 8.12.0 to 8.14.0.

See this package in npm:
https://www.npmjs.com/package/@fastify/swagger

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-22 07:46:59 +00:00
adc10cf675 fix: upgrade zod-to-json-schema from 3.22.0 to 3.22.4
Snyk has created this PR to upgrade zod-to-json-schema from 3.22.0 to 3.22.4.

See this package in npm:
https://www.npmjs.com/package/zod-to-json-schema

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-22 07:46:55 +00:00
49f7780e52 fix: upgrade bullmq from 5.1.1 to 5.1.6
Snyk has created this PR to upgrade bullmq from 5.1.1 to 5.1.6.

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

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-22 07:46:52 +00:00
26482c6b0a fix: upgrade mysql2 from 3.6.5 to 3.9.1
Snyk has created this PR to upgrade mysql2 from 3.6.5 to 3.9.1.

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

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-22 07:46:48 +00:00
1cf9aaeb1b Merge pull request #1400 from Infisical/daniel/ghost-users-full
(Feat): Interacting with projects programmatically
2024-02-22 02:40:48 -05:00
fed022ed09 feat: removed unused inviteSignup flag 2024-02-22 13:09:51 +05:30
64fbe4161c docs(sdk): fix typo PORT 2024-02-22 12:27:04 +05:30
bbe769a961 Increase SV migrate to 1000 & add billing page to Gamma 2024-02-22 07:30:26 +01:00
45772f0108 add envs 2024-02-22 01:27:02 -05: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
d9888f9dd1 envars update + docker standalone 2024-02-22 00:13:21 -05: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
f0e73474b7 add check out before integ tests run 2024-02-20 11:49:11 -05:00
7db829b0b5 Merge pull request #1408 from akhilmhdh/feat/integration-test-secret-ops
Integration test for secret ops
2024-02-20 11:40:26 -05:00
ccaa9fd96e add more test cases 2024-02-20 11:35:52 -05:00
b4db06c763 return empty string for decrypt fuction 2024-02-20 11:35:06 -05:00
3ebd2fdc6d Merge pull request #1426 from akhilmhdh/fix/identity-auth-identity-ops
feat: made identity crud with identity auth mode and api key for user me
2024-02-20 11:05:26 -05:00
8d06a6c969 feat: made identity crud with identity auth mode and api key for user me 2024-02-20 21:08:34 +05:30
2996efe9d5 feat: made secrets ops test to have both identity and jwt token based and test for identity 2024-02-20 20:58:10 +05:30
43879f6813 Update scanning-overview.mdx 2024-02-19 17:05:36 -08:00
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
d428fd055b update test envs 2024-02-19 16:52:59 -05:00
e4b89371f0 add env slug to make expect more strict 2024-02-19 16:08:21 -05:00
6f9b30b46e Minor UX adjustments to SCIM 2024-02-19 12:25:25 -08:00
35d589a15f add more secret test cases 2024-02-19 15:04:49 -05:00
8d77f2d8f3 Merge pull request #1420 from Infisical/snyk-upgrade-5b2dd8f68969929536cb2ba586aae1b7
[Snyk] Upgrade aws-sdk from 2.1532.0 to 2.1545.0
2024-02-19 13:55:11 -05:00
7070a69711 feat: made e2ee api test indepdent or stateless 2024-02-19 20:56:29 +05:30
7a65f8c837 Complete preliminary SCIM fns, add permissioning to SCIM, add docs for SCIM 2024-02-18 18:50:23 -08:00
678306b350 feat: some name changes for better understanding on testing 2024-02-18 22:51:10 +05:30
8864c811fe feat: updated to reuse and run integration api test before release 2024-02-18 22:47:24 +05:30
79206efcd0 fix: upgrade aws-sdk from 2.1532.0 to 2.1545.0
Snyk has created this PR to upgrade aws-sdk from 2.1532.0 to 2.1545.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

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-18 06:17:23 +00:00
06d30fc10f Merge pull request #1417 from Infisical/snyk-upgrade-7fe9aedc3b5777266707225885372828
[Snyk] Upgrade zustand from 4.4.7 to 4.5.0
2024-02-17 16:56:29 -05:00
abd28d9269 remove comment 2024-02-17 12:14:37 -05:00
c6c64b5499 trigger workflow 2024-02-17 12:12:12 -05:00
5481b84a94 patch package json 2024-02-17 16:34:27 +00:00
ab878e00c9 Merge pull request #1418 from akhilmhdh/fix/import-sec-500
fix: resolved secret import secrets failing when folder has empty secrets
2024-02-17 11:11:58 -05:00
6773996d40 fix: resolved secret import secrets failing when folder has empty secrets 2024-02-17 20:45:01 +05:30
2e20b38bce fix: upgrade zustand from 4.4.7 to 4.5.0
Snyk has created this PR to upgrade zustand from 4.4.7 to 4.5.0.

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

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:15 +00: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
2d088a865f Merge pull request #1412 from ruflair/patch-1
Update docker.mdx
2024-02-16 13:17:28 -05:00
0a8ec6b9da Update docker.mdx
Fix typo in docker.mdx
2024-02-16 10:13:29 -08:00
01b29c3917 Merge pull request #1336 from Infisical/snyk-fix-71fbba54e9eda81c90db390106760c64
[Snyk] Fix for 4 vulnerabilities
2024-02-16 12:15:16 -05:00
5439ddeadf Merge branch 'main' into snyk-fix-71fbba54e9eda81c90db390106760c64 2024-02-16 12:12:48 -05:00
9d17d5277b Merge pull request #1403 from Infisical/daniel/improve-reminders
(Fix): Improve reminders
2024-02-16 12:00:09 -05:00
c70fc7826a Merge pull request #1361 from Infisical/snyk-fix-4a9e6187125617ed814113514828d4f5
[Snyk] Security upgrade nodemailer from 6.9.7 to 6.9.9
2024-02-16 11:59:43 -05:00
9ed2bb38c3 Merge branch 'main' into snyk-fix-4a9e6187125617ed814113514828d4f5 2024-02-16 11:59:31 -05:00
f458cf0d40 Merge pull request #1123 from jinsley8/fix-webhook-settings-ui
fix(frontend): Remove max-width to match other views
2024-02-16 11:40:25 -05:00
ce3dc86f78 add troubleshoot for ansible 2024-02-16 10:37:34 -05:00
d1927cb9cf Merge pull request #1299 from ORCID/docs/ansible-workaround
docs: cover ansible forking error
2024-02-16 10:36:37 -05:00
e80426f72e update signup complete route 2024-02-16 09:15:52 -05:00
97d2a15d3e feat: updated admin dashboard to use zod and ts enum 2024-02-16 13:41:18 +05:30
f8a8ea2118 update interval text 2024-02-15 14:50:42 -05:00
f5cd68168b Merge pull request #1409 from Infisical/docker-compose-selfhost-docs
Docker compose docs with postgres
2024-02-15 12:58:06 -05:00
1a0a9a7402 docker compose docs with postgres 2024-02-15 12:54:22 -05:00
b74ce14d80 Update SecretItem.tsx 2024-02-15 18:52:25 +01:00
afdc5e8531 Merge pull request #1406 from abdulhakkeempa/bug-fix-model-close-on-cancel
(Fix): Create API Modal closes on Cancel button
2024-02-15 22:39:21 +05:30
b84579b866 feat(e2e): changed to root .env.test and added .env.test.example 2024-02-15 21:06:18 +05:30
4f3cf046fa feat(e2e): fixed post clean up error on gh be integration action 2024-02-15 20:43:40 +05:30
c71af00146 feat(e2e): added enc key on gh action for be test file 2024-02-15 20:38:44 +05:30
793440feb6 feat(e2e): made rebased changes 2024-02-15 20:30:12 +05:30
b24d748462 feat(e2e): added gh action for execution of integration tests on PR and release 2024-02-15 20:25:39 +05:30
4c49119ac5 feat(e2e): added identity token secret access integration tests 2024-02-15 20:25:39 +05:30
90f09c7a78 feat(e2e): added service token integration tests 2024-02-15 20:25:39 +05:30
00876f788c feat(e2e): added secrets integration tests 2024-02-15 20:25:39 +05:30
f09c48d79b feat(e2e): fixed seed file issue and added more seeding 2024-02-15 20:25:39 +05:30
57daeb71e6 Merge remote-tracking branch 'origin/main' into bug-fix-model-close-on-cancel 2024-02-15 10:47:50 +05:30
98b5f713a5 Fix: Cancel button working on Create API in Personal Settings 2024-02-15 10:09:45 +05:30
120d7e42bf Added secret reminder updating, and viewing existing values 2024-02-15 01:14:19 +01:00
c2bd259c12 Added secret reminders to sidebar (and formatting) 2024-02-15 01:14:05 +01:00
242d770098 Improved UX when trying to find reminder value 2024-02-15 01:12:48 +01:00
1855fc769d Update check-api-for-breaking-changes.yml 2024-02-14 15:42:45 -05:00
217fef65e8 update breaking change workflow to dev.yml 2024-02-14 15:37:21 -05:00
e15ed4cc58 Merge remote-tracking branch 'origin' into scim 2024-02-14 12:30:38 -08:00
8a0fd62785 update docker compose name 2024-02-14 15:19:47 -05:00
c69601c14e Merge pull request #1402 from Infisical/identity-access-token-ttl
Patch identity access token + client secret TTL
2024-02-14 12:19:45 -08:00
faf6323a58 Patch identity ttl conversion 2024-02-14 12:09:56 -08:00
a1cc118514 Merge remote-tracking branch 'origin/main' into feat-admin-signup 2024-02-14 23:46:25 +05:30
c73ee49425 Pass Okta SCIM 2.0 SPEC Test 2024-02-14 09:22:23 -08:00
ee69bccb6e Update disabled sign up routes 2024-02-14 14:18:44 +05:30
0ff3ddb0c8 Update generate schema run 2024-02-14 07:52:46 +05:30
b82d1b6a5d update docker compose on readme 2024-02-13 19:50:58 -05:00
3dcda44c50 Merge pull request #1385 from akhilmhdh/feat/init-container
feat: changed docker compose to use init container pattern for running migration
2024-02-13 19:46:39 -05:00
f320b08ca8 rename docker compose and bring back make up-dev 2024-02-13 19:44:42 -05:00
df6e5674cf patch breaking change ci 2024-02-13 19:10:28 -05:00
6bac143a8e test without pg connection 2024-02-13 19:06:47 -05:00
38b93e499f logs on health check 2024-02-13 18:58:13 -05:00
a521538010 show all logs 2024-02-13 18:47:12 -05:00
8cc2553452 test workflow no db connection 2024-02-13 18:43:43 -05:00
b1cb9de001 correct -d mode 2024-02-13 18:39:28 -05:00
036256b350 add back -d mode 2024-02-13 18:37:04 -05:00
d3a06b82e6 update health check 2024-02-13 18:34:15 -05:00
87436cfb57 Update check-api-for-breaking-changes.yml 2024-02-13 18:04:49 -05:00
5c58a4d1a3 added signup event and restyled admin flow 2024-02-13 13:15:54 -08:00
03a91b2c59 Merge pull request #1387 from akhilmhdh/chore/doc-openapi
chore: changed mintlify to directly get from prod openapi
2024-02-13 00:42:47 -05:00
751361bd54 add new propety to api 2024-02-13 00:38:59 -05:00
b4b88daf36 Revert "test breaking change"
This reverts commit 6546740bd9.
2024-02-13 00:37:49 -05:00
6546740bd9 test breaking change 2024-02-13 00:34:31 -05:00
b32558c66f add . 2024-02-13 00:29:49 -05:00
effd30857e fix typo 2024-02-12 22:45:35 -05:00
60998c8944 Merge pull request #1369 from akhilmhdh/chore/feature-x-guide
feat: added guides for new backend development
2024-02-12 22:35:16 -05:00
3c4d9fd4a9 delete docs in backend 2024-02-12 22:34:36 -05:00
ad70c783e8 add backend guide to contributor 2024-02-12 22:33:55 -05:00
7347362738 rephrase new feature development guide 2024-02-12 21:49:15 -05:00
4b7f2e808b Update overview.mdx 2024-02-12 15:06:56 -08:00
57f9d13189 Merge pull request #1397 from Infisical/infisical-jenkins
Rewrite Infisical Jenkins docs
2024-02-12 18:02:11 -05:00
bd2e8ac922 rewrite jenkins docs 2024-02-12 18:01:20 -05:00
79694750af Remove signup disable check for SAML 2024-02-12 11:43:55 -08:00
03db367a4e Merge pull request #1396 from Infisical/azure-saml
Add disableRequestedAuthnContext for azure saml
2024-02-12 10:40:19 -08:00
b0fb848a92 Add disableRequestedAuthnContext for azure saml 2024-02-12 10:35:38 -08:00
3a7b697549 Make progress on SCIM 2024-02-12 09:18:33 -08:00
4fdfcd50dc feat: changed check-api-breaking with oasdiff 2024-02-12 22:32:02 +05:30
db205b855a feat: removed mongo comments from compose file 2024-02-12 14:52:19 +05:30
e707f0d235 feat: added description and security over api written in docs 2024-02-12 14:19:49 +05:30
27f4225c44 Merge pull request #1391 from Infisical/ecs-docs
ECS docs with agent
2024-02-11 19:17:01 -05:00
28a9d8e739 complete ecs dcs 2024-02-11 19:14:14 -05:00
0fb87ab05f Update move to react hook form, rename allowedSignUpDomain 2024-02-12 04:09:46 +05:30
2ef8781378 Update support multiple domain and error handling 2024-02-11 10:55:36 +05:30
3f96f0a8fb Fix dropdown not working on page transition 2024-02-11 00:40:17 +05:30
da377f6fda Update error handling and refactor 2024-02-10 23:00:16 +05:30
5cf1ec2400 Add restrict signup based on domain 2024-02-10 10:44:39 +05:30
a1321e4749 aws ecs with agent docs 2024-02-09 22:31:29 -05:00
6c1489a87b Add admin invite only signup field 2024-02-10 03:26:56 +05:30
d4db01bbde Merge pull request #1388 from Infisical/azure-ad
Correct spInitiated spelling in Azure SAML
2024-02-09 12:31:41 -08:00
39634b8aae Correct spInitiated spelling in Azure SAML 2024-02-09 12:17:29 -08:00
4815ff13ee Update SAML SSO docs to include enforce SAML SSO toggle 2024-02-09 10:36:23 -08:00
fb503756d9 Merge pull request #1372 from Infisical/org-based-auth
Org-Level Auth Enforcement for SAML Orgs and Enhancements for SAML SSO
2024-02-09 13:15:08 -05:00
069b0cd6fb Fix lint issue, add backend permission check for SAML SSO enforcement toggle 2024-02-09 10:02:12 -08:00
ed23bd40d2 Redirect to SAML portal upon enforcing SAML SSO 2024-02-09 09:50:20 -08:00
82181f078a Patch login with SAML when config is inactive 2024-02-09 09:35:51 -08:00
48a97fb39d chore: changed mintlify to directly get from prod openapi 2024-02-09 22:53:27 +05:30
eeaee4409c revert swap 2024-02-09 12:08:11 -05:00
a9a5e92358 Empty 2024-02-09 09:02:22 -08:00
8d457bb0bf swap src des 2024-02-09 12:01:21 -05:00
5878a221f8 Run lint fix 2024-02-09 08:44:30 -08:00
fdbf59cd78 Redirect users to SAML portal on change org to SAML org 2024-02-09 08:31:19 -08:00
d8ea26feb7 feat: changed docker compose to use init container pattern for migration 2024-02-09 13:12:25 +05:30
2cc2a91812 Change update slug fallback to undefined 2024-02-08 17:47:58 -08:00
92828b5295 add slug index 2024-02-08 20:47:33 -05:00
50c0fae557 continue ecs docs 2024-02-08 20:29:38 -05:00
4e2f2281f9 Update orgScope to orgId naming for org-level auth ref, rewire user invite saml block to org authEnforce field 2024-02-08 17:29:21 -08:00
d5064fe75a Start SCIM functionality 2024-02-08 15:54:20 -08:00
70e083bae0 feat: open api-diff added detach mode 2024-02-08 22:20:01 +05:30
6a943e275a Updated open-api diff gh action host docker ip 2024-02-08 22:05:35 +05:30
526dc6141b remove -d mode on docker compose 2024-02-08 11:04:26 -05:00
dcab9dcdda update docker compose up 2024-02-08 10:50:25 -05:00
1b0591def8 fix gha breaking change 2024-02-07 16:17:12 -05:00
4b4305bddc Merge pull request #1375 from akhilmhdh/feat/api-diff
feat: github workflow for api diff check, ts check and lint check on PR
2024-02-07 15:26:04 -05:00
22d89d791c Patch new org creation condition on SAML account signup, enable users to toggle auth methods regardless of what org they are in 2024-02-07 12:13:09 -08:00
fcaff76afa rename git hub action 2024-02-07 14:34:02 -05:00
ae9eb20189 set license server url default 2024-02-07 13:51:55 -05:00
3905d16a7c fix license server axios typo 2024-02-07 13:48:09 -05:00
ecafdb0d01 patch check for version 2024-02-07 13:11:53 -05:00
3f8ce42682 Merge remote-tracking branch 'origin' into org-based-auth 2024-02-07 09:49:13 -08:00
3ecfb3f9d2 Show usage and billing tab on cloud only 2024-02-07 09:45:42 -08:00
9011394c34 Add validation to org slug 2024-02-07 09:39:06 -08:00
c0096ca64c Merge pull request #1378 from Infisical/patch-service-token-fetch
patch get secret by name
2024-02-07 23:00:20 +05:30
8bc952388c add log 2024-02-07 12:23:48 -05:00
eef29cd2d4 patch get secret by name 2024-02-07 12:11:58 -05:00
6ef873f3a0 Merge pull request #1377 from Infisical/allow-name-initial-org
add initial org rename
2024-02-07 20:51:30 +05:30
8313245ae1 feat: github workflow for api diff check, ts check and lint check on PR 2024-02-07 15:13:08 +05:30
fc7015de83 Add lockout-preventative step in saml config setup, add update org slug section in org settings, revise navigate to org flow to account for org-level auth enforced orgs 2024-02-06 15:51:24 -08:00
c1aa5c840c Add org-scoped auth to project-level endpoints 2024-02-05 14:48:02 -08:00
dc146d0883 feat: fixed spelling errors 2024-02-05 22:28:40 +05:30
24dd79b566 feat: added guides for new backend development 2024-02-05 16:31:03 +05:30
410476ecb5 Merge remote-tracking branch 'origin' into org-based-auth 2024-02-04 15:03:43 -08:00
f1c41be7d4 Resolve merge conflicts 2024-02-04 15:02:43 -08:00
f138973ac7 Add org-scoped auth to org-level endpoints, add migration file for org enableAuth field 2024-02-04 14:44:08 -08:00
00650df501 fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NODEMAILER-6219989
2024-02-01 20:01:54 +00:00
44f087991c ECS documentation 2024-01-31 23:53:41 -05:00
6ff5fb69d4 fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-6124857
- https://snyk.io/vuln/SNYK-JS-AXIOS-6144788
- https://snyk.io/vuln/SNYK-JS-FASTIFYSWAGGERUI-6157561
- https://snyk.io/vuln/SNYK-JS-INFLIGHT-6095116
2024-01-27 15:06:50 +00:00
9fe2021d9f docs: cover ansible forking error 2024-01-10 22:33:38 +00:00
fe2f2f972e fix(frontend): Remove max-width to match other views
This commit removes the max-width constraint on the WebhooksTab.tsx component, aligning it with the full-width layout consistency seen in other views. The previous max-width of 1024px resulted in unused space on larger screens.
2023-10-25 13:54:09 -04:00
364 changed files with 13462 additions and 10626 deletions

View File

@ -3,16 +3,18 @@
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION # THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# Required
DB_CONNECTION_URI=postgres://infisical:infisical@db:5432/infisical
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION # THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE= AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
# MongoDB # Postgres creds
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref POSTGRES_PASSWORD=infisical
# to the MongoDB container instance or Mongo Cloud POSTGRES_USER=infisical
# Required POSTGRES_DB=infisical
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# Redis # Redis
REDIS_URL=redis://redis:6379 REDIS_URL=redis://redis:6379

1
.env.migration.example Normal file
View File

@ -0,0 +1 @@
DB_CONNECTION_URI=

4
.env.test.example Normal file
View File

@ -0,0 +1,4 @@
REDIS_URL=redis://localhost:6379
DB_CONNECTION_URI=postgres://infisical:infisical@localhost/infisical?sslmode=disable
AUTH_SECRET=4bnfe4e407b8921c104518903515b218
ENCRYPTION_KEY=4bnfe4e407b8921c104518903515b218

View File

@ -0,0 +1,75 @@
name: "Check API For Breaking Changes"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/src/server/routes/**"
jobs:
check-be-api-changes:
name: Check API Changes
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout source
uses: actions/checkout@v3
# - name: Setup Node 20
# uses: actions/setup-node@v3
# with:
# node-version: "20"
# uncomment this when testing locally using nektos/act
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
with:
version: "2.14.2"
- name: 📦Build the latest image
run: docker build --tag infisical-api .
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
- name: Start the server
run: |
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
env:
REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
JWT_AUTH_SECRET: something-random
- uses: actions/setup-go@v5
with:
go-version: '1.21.5'
- name: Wait for container to be stable and check logs
run: |
SECONDS=0
HEALTHY=0
while [ $SECONDS -lt 60 ]; do
if docker ps | grep infisical-api | grep -q healthy; then
echo "Container is healthy."
HEALTHY=1
break
fi
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
docker logs infisical-api
sleep 2
SECONDS=$((SECONDS+2))
done
if [ $HEALTHY -ne 1 ]; then
echo "Container did not become healthy in time"
exit 1
fi
- name: Install openapi-diff
run: go install github.com/tufin/oasdiff@latest
- name: Running OpenAPI Spec diff action
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker stop infisical-api
docker remove infisical-api

View File

@ -1,43 +0,0 @@
name: "Check Backend Pull Request"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
jobs:
check-be-pr:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Setup Node 16
uses: actions/setup-node@v3
with:
node-version: "16"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: 📦 Install dependencies
run: npm ci --only-production
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
# - name: 📁 Upload test results
# uses: actions/upload-artifact@v3
# if: always()
# with:
# name: be-test-results
# path: |
# ./backend/reports
# ./backend/coverage
- name: 🏗️ Run build
run: npm run build
working-directory: backend

View File

@ -0,0 +1,35 @@
name: "Check Backend PR types and lint"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
jobs:
check-be-pr:
name: Check TS and Lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
run: npm install
working-directory: backend
- name: Run type check
run: npm run type:check
working-directory: backend
- name: Run lint check
run: npm run lint
working-directory: backend

View File

@ -1,4 +1,4 @@
name: Check Frontend Pull Request name: Check Frontend Type and Lint check
on: on:
pull_request: pull_request:
@ -10,8 +10,8 @@ on:
- "frontend/.eslintrc.js" - "frontend/.eslintrc.js"
jobs: jobs:
check-fe-pr: check-fe-ts-lint:
name: Check name: Check Frontend Type and Lint check
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
@ -25,12 +25,11 @@ jobs:
cache: "npm" cache: "npm"
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
- name: 📦 Install dependencies - name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts run: npm install
working-directory: frontend working-directory: frontend
# - - name: 🏗️ Run Type check
# name: 🧪 Run tests run: npm run type:check
# run: npm run test:ci working-directory: frontend
# working-directory: frontend - name: 🏗️ Run Link check
- name: 🏗️ Run build run: npm run lint:fix
run: npm run build
working-directory: frontend working-directory: frontend

View File

@ -5,9 +5,14 @@ on:
- "infisical/v*.*.*-postgres" - "infisical/v*.*.*-postgres"
jobs: jobs:
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone: infisical-standalone:
name: Build infisical standalone image postgres name: Build infisical standalone image postgres
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [infisical-tests]
steps: steps:
- name: Extract version from tag - name: Extract version from tag
id: extract_version id: extract_version

47
.github/workflows/run-backend-tests.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: "Run backend tests"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
workflow_call:
jobs:
check-be-pr:
name: Run integration test
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
with:
version: "2.14.2"
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
run: npm install
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test
run: npm run test:e2e
working-directory: backend
env:
REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
AUTH_SECRET: something-random
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down

4
.gitignore vendored
View File

@ -6,7 +6,7 @@ node_modules
.env.gamma .env.gamma
.env.prod .env.prod
.env.infisical .env.infisical
.env.migration
*~ *~
*.swp *.swp
*.swo *.swo
@ -63,3 +63,5 @@ yarn-error.log*
.vscode/* .vscode/*
frontend-build frontend-build
*.tgz

View File

@ -5,16 +5,10 @@ push:
docker-compose -f docker-compose.yml push docker-compose -f docker-compose.yml push
up-dev: up-dev:
docker-compose -f docker-compose.dev.yml up --build docker compose -f docker-compose.dev.yml up --build
up-pg-dev:
docker compose -f docker-compose.pg.yml up --build
i-dev:
infisical run -- docker-compose -f docker-compose.dev.yml up --build
up-prod: up-prod:
docker-compose -f docker-compose.yml up --build docker-compose -f docker-compose.prod.yml up --build
down: down:
docker-compose down docker-compose down

View File

@ -84,13 +84,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
Linux/macOS: Linux/macOS:
```console ```console
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.yml up git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.prod.yml up
``` ```
Windows Command Prompt: Windows Command Prompt:
```console ```console
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.yml up git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.prod.yml up
``` ```
Create an account at `http://localhost:80` Create an account at `http://localhost:80`

View File

@ -1,2 +1,3 @@
vitest-environment-infisical.ts vitest-environment-infisical.ts
vitest.config.ts vitest.config.ts
vitest.e2e.config.ts

View File

@ -21,6 +21,18 @@ module.exports = {
tsconfigRootDir: __dirname tsconfigRootDir: __dirname
}, },
root: true, root: true,
overrides: [
{
files: ["./e2e-test/**/*"],
rules: {
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off",
}
}
],
rules: { rules: {
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off", "@typescript-eslint/no-unsafe-enum-comparison": "off",

View File

@ -0,0 +1,71 @@
import { OrgMembershipRole } from "@app/db/schemas";
import { seedData1 } from "@app/db/seed-data";
export const createIdentity = async (name: string, role: string) => {
const createIdentityRes = await testServer.inject({
method: "POST",
url: "/api/v1/identities",
body: {
name,
role,
organizationId: seedData1.organization.id
},
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(createIdentityRes.statusCode).toBe(200);
return createIdentityRes.json().identity;
};
export const deleteIdentity = async (id: string) => {
const deleteIdentityRes = await testServer.inject({
method: "DELETE",
url: `/api/v1/identities/${id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(deleteIdentityRes.statusCode).toBe(200);
return deleteIdentityRes.json().identity;
};
describe("Identity v1", async () => {
test("Create identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull();
await deleteIdentity(newIdentity.id);
});
test("Update identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull();
const updatedIdentity = await testServer.inject({
method: "PATCH",
url: `/api/v1/identities/${newIdentity.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: "updated-mac-1",
role: OrgMembershipRole.Member
}
});
expect(updatedIdentity.statusCode).toBe(200);
expect(updatedIdentity.json().identity.name).toBe("updated-mac-1");
await deleteIdentity(newIdentity.id);
});
test("Delete Identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
const deletedIdentity = await deleteIdentity(newIdentity.id);
expect(deletedIdentity.name).toBe("mac1");
});
});

View File

@ -1,6 +1,7 @@
import { seedData1 } from "@app/db/seed-data";
import jsrp from "jsrp"; import jsrp from "jsrp";
import { seedData1 } from "@app/db/seed-data";
describe("Login V1 Router", async () => { describe("Login V1 Router", async () => {
// eslint-disable-next-line // eslint-disable-next-line
const client = new jsrp.client(); const client = new jsrp.client();

View File

@ -1,6 +1,40 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project"; import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project";
const createProjectEnvironment = async (name: string, slug: string) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name,
slug
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
return payload.environment;
};
const deleteProjectEnvironment = async (envId: string) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/workspace/${seedData1.project.id}/environments/${envId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
return payload.environment;
};
describe("Project Environment Router", async () => { describe("Project Environment Router", async () => {
test("Get default environments", async () => { test("Get default environments", async () => {
const res = await testServer.inject({ const res = await testServer.inject({
@ -31,24 +65,10 @@ describe("Project Environment Router", async () => {
expect(payload.workspace.environments.length).toBe(3); expect(payload.workspace.environments.length).toBe(3);
}); });
const mockProjectEnv = { name: "temp", slug: "temp", id: "" }; // id will be filled in create op const mockProjectEnv = { name: "temp", slug: "temp" }; // id will be filled in create op
test("Create environment", async () => { test("Create environment", async () => {
const res = await testServer.inject({ const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
method: "POST", expect(newEnvironment).toEqual(
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: mockProjectEnv.name,
slug: mockProjectEnv.slug
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
name: mockProjectEnv.name, name: mockProjectEnv.name,
@ -59,14 +79,15 @@ describe("Project Environment Router", async () => {
updatedAt: expect.any(String) updatedAt: expect.any(String)
}) })
); );
mockProjectEnv.id = payload.environment.id; await deleteProjectEnvironment(newEnvironment.id);
}); });
test("Update environment", async () => { test("Update environment", async () => {
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
const updatedName = { name: "temp#2", slug: "temp2" }; const updatedName = { name: "temp#2", slug: "temp2" };
const res = await testServer.inject({ const res = await testServer.inject({
method: "PATCH", method: "PATCH",
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`, url: `/api/v1/workspace/${seedData1.project.id}/environments/${newEnvironment.id}`,
headers: { headers: {
authorization: `Bearer ${jwtAuthToken}` authorization: `Bearer ${jwtAuthToken}`
}, },
@ -82,7 +103,7 @@ describe("Project Environment Router", async () => {
expect(payload).toHaveProperty("environment"); expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual( expect(payload.environment).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: newEnvironment.id,
name: updatedName.name, name: updatedName.name,
slug: updatedName.slug, slug: updatedName.slug,
projectId: seedData1.project.id, projectId: seedData1.project.id,
@ -91,61 +112,21 @@ describe("Project Environment Router", async () => {
updatedAt: expect.any(String) updatedAt: expect.any(String)
}) })
); );
mockProjectEnv.name = updatedName.name; await deleteProjectEnvironment(newEnvironment.id);
mockProjectEnv.slug = updatedName.slug;
}); });
test("Delete environment", async () => { test("Delete environment", async () => {
const res = await testServer.inject({ const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
method: "DELETE", const deletedProjectEnvironment = await deleteProjectEnvironment(newEnvironment.id);
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`, expect(deletedProjectEnvironment).toEqual(
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: deletedProjectEnvironment.id,
name: mockProjectEnv.name, name: mockProjectEnv.name,
slug: mockProjectEnv.slug, slug: mockProjectEnv.slug,
position: 1, position: 4,
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String) updatedAt: expect.any(String)
}) })
); );
}); });
// after all these opreations the list of environment should be still same
test("Default list of environment", async () => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/workspace/${seedData1.project.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("workspace");
// check for default environments
expect(payload).toEqual({
workspace: expect.objectContaining({
name: seedData1.project.name,
id: seedData1.project.id,
slug: seedData1.project.slug,
environments: expect.arrayContaining([
expect.objectContaining(DEFAULT_PROJECT_ENVS[0]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[1]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[2])
])
})
});
// ensure only two default environments exist
expect(payload.workspace.environments.length).toBe(3);
});
}); });

View File

@ -1,12 +1,6 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
describe("Secret Folder Router", async () => { const createFolder = async (dto: { path: string; name: string }) => {
test.each([
{ name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
])("Create folder $name in $path", async ({ name, path }) => {
const res = await testServer.inject({ const res = await testServer.inject({
method: "POST", method: "POST",
url: `/api/v1/folders`, url: `/api/v1/folders`,
@ -16,21 +10,47 @@ describe("Secret Folder Router", async () => {
body: { body: {
workspaceId: seedData1.project.id, workspaceId: seedData1.project.id,
environment: seedData1.environment.slug, environment: seedData1.environment.slug,
name, name: dto.name,
path path: dto.path
} }
}); });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload); return res.json().folder;
expect(payload).toHaveProperty("folder"); };
const deleteFolder = async (dto: { path: string; id: string }) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${dto.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: dto.path
}
});
expect(res.statusCode).toBe(200);
return res.json().folder;
};
describe("Secret Folder Router", async () => {
test.each([
{ name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name });
// check for default environments // check for default environments
expect(payload).toEqual({ expect(createdFolder).toEqual(
folder: expect.objectContaining({ expect.objectContaining({
name, name,
id: expect.any(String) id: expect.any(String)
}) })
}); );
await deleteFolder({ path, id: createdFolder.id });
}); });
test.each([ test.each([
@ -43,6 +63,8 @@ describe("Secret Folder Router", async () => {
}, },
{ path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } } { path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } }
])("Get folders $path", async ({ path, expected }) => { ])("Get folders $path", async ({ path, expected }) => {
const newFolders = await Promise.all(expected.folders.map(({ name }) => createFolder({ name, path })));
const res = await testServer.inject({ const res = await testServer.inject({
method: "GET", method: "GET",
url: `/api/v1/folders`, url: `/api/v1/folders`,
@ -59,36 +81,22 @@ describe("Secret Folder Router", async () => {
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload); const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folders"); expect(payload).toHaveProperty("folders");
expect(payload.folders.length).toBe(expected.length); expect(payload.folders.length >= expected.folders.length).toBeTruthy();
expect(payload).toEqual({ folders: expected.folders.map((el) => expect.objectContaining(el)) }); expect(payload).toEqual({
folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
});
await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id })));
}); });
let toBeDeleteFolderId = "";
test("Update a deep folder", async () => { test("Update a deep folder", async () => {
const res = await testServer.inject({ const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
method: "PATCH", expect(newFolder).toEqual(
url: `/api/v1/folders/folder1`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-updated",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folder");
expect(payload.folder).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
name: "folder-updated" name: "folder-updated"
}) })
); );
toBeDeleteFolderId = payload.folder.id;
const resUpdatedFolders = await testServer.inject({ const resUpdatedFolders = await testServer.inject({
method: "GET", method: "GET",
@ -106,14 +114,16 @@ describe("Secret Folder Router", async () => {
expect(resUpdatedFolders.statusCode).toBe(200); expect(resUpdatedFolders.statusCode).toBe(200);
const updatedFolderList = JSON.parse(resUpdatedFolders.payload); const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
expect(updatedFolderList).toHaveProperty("folders"); expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(1);
expect(updatedFolderList.folders[0].name).toEqual("folder-updated"); expect(updatedFolderList.folders[0].name).toEqual("folder-updated");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
}); });
test("Delete a deep folder", async () => { test("Delete a deep folder", async () => {
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
const res = await testServer.inject({ const res = await testServer.inject({
method: "DELETE", method: "DELETE",
url: `/api/v1/folders/${toBeDeleteFolderId}`, url: `/api/v1/folders/${newFolder.id}`,
headers: { headers: {
authorization: `Bearer ${jwtAuthToken}` authorization: `Bearer ${jwtAuthToken}`
}, },

View File

@ -1,10 +1,6 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
describe("Secret Folder Router", async () => { const createSecretImport = async (importPath: string, importEnv: string) => {
test.each([
{ importEnv: "dev", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
const res = await testServer.inject({ const res = await testServer.inject({
method: "POST", method: "POST",
url: `/api/v1/secret-imports`, url: `/api/v1/secret-imports`,
@ -25,8 +21,37 @@ describe("Secret Folder Router", async () => {
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload); const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport"); expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
const deleteSecretImport = async (id: string) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/secret-imports/${id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
describe("Secret Import Router", async () => {
test.each([
{ importEnv: "dev", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
// check for default environments // check for default environments
expect(payload.secretImport).toEqual( const payload = await createSecretImport(importPath, importEnv);
expect(payload).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
importPath: expect.any(String), importPath: expect.any(String),
@ -37,10 +62,12 @@ describe("Secret Folder Router", async () => {
}) })
}) })
); );
await deleteSecretImport(payload.id);
}); });
let testSecretImportId = "";
test("Get secret imports", async () => { test("Get secret imports", async () => {
const createdImport1 = await createSecretImport("/", "dev");
const createdImport2 = await createSecretImport("/", "staging");
const res = await testServer.inject({ const res = await testServer.inject({
method: "GET", method: "GET",
url: `/api/v1/secret-imports`, url: `/api/v1/secret-imports`,
@ -58,7 +85,6 @@ describe("Secret Folder Router", async () => {
const payload = JSON.parse(res.payload); const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImports"); expect(payload).toHaveProperty("secretImports");
expect(payload.secretImports.length).toBe(2); expect(payload.secretImports.length).toBe(2);
testSecretImportId = payload.secretImports[0].id;
expect(payload.secretImports).toEqual( expect(payload.secretImports).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@ -72,12 +98,20 @@ describe("Secret Folder Router", async () => {
}) })
]) ])
); );
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
}); });
test("Update secret import position", async () => { test("Update secret import position", async () => {
const res = await testServer.inject({ const devImportDetails = { path: "/", envSlug: "dev" };
const stagingImportDetails = { path: "/", envSlug: "staging" };
const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug);
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
const updateImportRes = await testServer.inject({
method: "PATCH", method: "PATCH",
url: `/api/v1/secret-imports/${testSecretImportId}`, url: `/api/v1/secret-imports/${createdImport1.id}`,
headers: { headers: {
authorization: `Bearer ${jwtAuthToken}` authorization: `Bearer ${jwtAuthToken}`
}, },
@ -91,8 +125,8 @@ describe("Secret Folder Router", async () => {
} }
}); });
expect(res.statusCode).toBe(200); expect(updateImportRes.statusCode).toBe(200);
const payload = JSON.parse(res.payload); const payload = JSON.parse(updateImportRes.payload);
expect(payload).toHaveProperty("secretImport"); expect(payload).toHaveProperty("secretImport");
// check for default environments // check for default environments
expect(payload.secretImport).toEqual( expect(payload.secretImport).toEqual(
@ -102,7 +136,7 @@ describe("Secret Folder Router", async () => {
position: 2, position: 2,
importEnv: expect.objectContaining({ importEnv: expect.objectContaining({
name: expect.any(String), name: expect.any(String),
slug: expect.any(String), slug: expect.stringMatching(devImportDetails.envSlug),
id: expect.any(String) id: expect.any(String)
}) })
}) })
@ -124,28 +158,19 @@ describe("Secret Folder Router", async () => {
expect(secretImportsListRes.statusCode).toBe(200); expect(secretImportsListRes.statusCode).toBe(200);
const secretImportList = JSON.parse(secretImportsListRes.payload); const secretImportList = JSON.parse(secretImportsListRes.payload);
expect(secretImportList).toHaveProperty("secretImports"); expect(secretImportList).toHaveProperty("secretImports");
expect(secretImportList.secretImports[1].id).toEqual(testSecretImportId); expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id);
expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id);
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
}); });
test("Delete secret import position", async () => { test("Delete secret import position", async () => {
const res = await testServer.inject({ const createdImport1 = await createSecretImport("/", "dev");
method: "DELETE", const createdImport2 = await createSecretImport("/", "staging");
url: `/api/v1/secret-imports/${testSecretImportId}`, const deletedImport = await deleteSecretImport(createdImport1.id);
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
// check for default environments // check for default environments
expect(payload.secretImport).toEqual( expect(deletedImport).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
importPath: expect.any(String), importPath: expect.any(String),
@ -175,5 +200,7 @@ describe("Secret Folder Router", async () => {
expect(secretImportList).toHaveProperty("secretImports"); expect(secretImportList).toHaveProperty("secretImports");
expect(secretImportList.secretImports.length).toEqual(1); expect(secretImportList.secretImports.length).toEqual(1);
expect(secretImportList.secretImports[0].position).toEqual(1); expect(secretImportList.secretImports[0].position).toEqual(1);
await deleteSecretImport(createdImport2.id);
}); });
}); });

View File

@ -0,0 +1,579 @@
import crypto from "node:crypto";
import { SecretType, TSecrets } from "@app/db/schemas";
import { decryptSecret, encryptSecret, getUserPrivateKey, seedData1 } from "@app/db/seed-data";
import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
const createServiceToken = async (
scopes: { environment: string; secretPath: string }[],
permissions: ("read" | "write")[]
) => {
const projectKeyRes = await testServer.inject({
method: "GET",
url: `/api/v2/workspace/${seedData1.project.id}/encrypted-key`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
const projectKeyEnc = JSON.parse(projectKeyRes.payload);
const userInfoRes = await testServer.inject({
method: "GET",
url: "/api/v2/users/me",
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
const { user: userInfo } = JSON.parse(userInfoRes.payload);
const privateKey = await getUserPrivateKey(seedData1.password, userInfo);
const projectKey = decryptAsymmetric({
ciphertext: projectKeyEnc.encryptedKey,
nonce: projectKeyEnc.nonce,
publicKey: projectKeyEnc.sender.publicKey,
privateKey
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(projectKey, randomBytes);
const serviceTokenRes = await testServer.inject({
method: "POST",
url: "/api/v2/service-token",
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: "test-token",
workspaceId: seedData1.project.id,
scopes,
encryptedKey: ciphertext,
iv,
tag,
permissions,
expiresIn: null
}
});
expect(serviceTokenRes.statusCode).toBe(200);
const serviceTokenInfo = serviceTokenRes.json();
expect(serviceTokenInfo).toHaveProperty("serviceToken");
expect(serviceTokenInfo).toHaveProperty("serviceTokenData");
return `${serviceTokenInfo.serviceToken}.${randomBytes}`;
};
const deleteServiceToken = async () => {
const serviceTokenListRes = await testServer.inject({
method: "GET",
url: `/api/v1/workspace/${seedData1.project.id}/service-token-data`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(serviceTokenListRes.statusCode).toBe(200);
const serviceTokens = JSON.parse(serviceTokenListRes.payload).serviceTokenData as { name: string; id: string }[];
expect(serviceTokens.length).toBeGreaterThan(0);
const serviceTokenInfo = serviceTokens.find(({ name }) => name === "test-token");
expect(serviceTokenInfo).toBeDefined();
const deleteTokenRes = await testServer.inject({
method: "DELETE",
url: `/api/v2/service-token/${serviceTokenInfo?.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(deleteTokenRes.statusCode).toBe(200);
};
const createSecret = async (dto: {
projectKey: string;
path: string;
key: string;
value: string;
comment: string;
type?: SecretType;
token: string;
}) => {
const createSecretReqBody = {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: dto.type || SecretType.Shared,
secretPath: dto.path,
...encryptSecret(dto.projectKey, dto.key, dto.value, dto.comment)
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/${dto.key}`,
headers: {
authorization: `Bearer ${dto.token}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
const createdSecretPayload = JSON.parse(createSecRes.payload);
expect(createdSecretPayload).toHaveProperty("secret");
return createdSecretPayload.secret;
};
const deleteSecret = async (dto: { path: string; key: string; token: string }) => {
const deleteSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/${dto.key}`,
headers: {
authorization: `Bearer ${dto.token}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: dto.path
}
});
expect(deleteSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
return updatedSecretPayload.secret;
};
describe("Service token secret ops", async () => {
let serviceToken = "";
let projectKey = "";
let folderId = "";
beforeAll(async () => {
serviceToken = await createServiceToken(
[{ secretPath: "/**", environment: seedData1.environment.slug }],
["read", "write"]
);
// this is ensure cli service token decryptiong working fine
const serviceTokenInfoRes = await testServer.inject({
method: "GET",
url: "/api/v2/service-token",
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(serviceTokenInfoRes.statusCode).toBe(200);
const serviceTokenInfo = serviceTokenInfoRes.json();
const serviceTokenParts = serviceToken.split(".");
projectKey = decryptSymmetric128BitHexKeyUTF8({
key: serviceTokenParts[3],
tag: serviceTokenInfo.tag,
ciphertext: serviceTokenInfo.encryptedKey,
iv: serviceTokenInfo.iv
});
// create a deep folder
const folderCreate = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder",
path: "/nested1/nested2"
}
});
expect(folderCreate.statusCode).toBe(200);
folderId = folderCreate.json().folder.id;
});
afterAll(async () => {
await deleteServiceToken();
// create a deep folder
const deleteFolder = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${folderId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/nested1/nested2"
}
});
expect(deleteFolder.statusCode).toBe(200);
});
const testSecrets = [
{
path: "/",
secret: {
key: "ST-SEC",
value: "something-secret",
comment: "some comment"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "NESTED-ST-SEC",
value: "something-secret",
comment: "some comment"
}
}
];
const getSecrets = async (environment: string, secretPath = "/") => {
const res = await testServer.inject({
method: "GET",
url: `/api/v3/secrets`,
headers: {
authorization: `Bearer ${serviceToken}`
},
query: {
secretPath,
environment,
workspaceId: seedData1.project.id
}
});
const secrets: TSecrets[] = JSON.parse(res.payload).secrets || [];
return secrets.map((el) => ({ ...decryptSecret(projectKey, el), type: el.type }));
};
test.each(testSecrets)("Create secret in path $path", async ({ secret, path }) => {
const createdSecret = await createSecret({ projectKey, path, ...secret, token: serviceToken });
const decryptedSecret = decryptSecret(projectKey, createdSecret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual(secret.value);
expect(decryptedSecret.comment).toEqual(secret.comment);
expect(decryptedSecret.version).toEqual(1);
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: secret.key,
value: secret.value,
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Get secret by name in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const getSecByNameRes = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${serviceToken}`
},
query: {
secretPath: path,
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug
}
});
expect(getSecByNameRes.statusCode).toBe(200);
const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload);
expect(getSecretByNamePayload).toHaveProperty("secret");
const decryptedSecret = decryptSecret(projectKey, getSecretByNamePayload.secret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual(secret.value);
expect(decryptedSecret.comment).toEqual(secret.comment);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Update secret in path $path", async ({ path, secret }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const updateSecretReqBody = {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: SecretType.Shared,
secretPath: path,
...encryptSecret(projectKey, secret.key, "new-value", secret.comment)
};
const updateSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: updateSecretReqBody
});
expect(updateSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
const decryptedSecret = decryptSecret(projectKey, updatedSecretPayload.secret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual("new-value");
expect(decryptedSecret.comment).toEqual(secret.comment);
// list secret should have updated value
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: secret.key,
value: "new-value",
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Delete secret in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const deletedSecret = await deleteSecret({ path, key: secret.key, token: serviceToken });
const decryptedSecret = decryptSecret(projectKey, deletedSecret);
expect(decryptedSecret.key).toEqual(secret.key);
// shared secret deletion should delete personal ones also
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
key: secret.key,
type: SecretType.Shared
})
])
);
});
test.each(testSecrets)("Bulk create secrets in path $path", async ({ secret, path }) => {
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment)
}))
}
});
expect(createSharedSecRes.statusCode).toBe(200);
const createSharedSecPayload = JSON.parse(createSharedSecRes.payload);
expect(createSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.key}-${i + 1}`,
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
deleteSecret({ path, token: serviceToken, key: `BULK-${secret.key}-${i + 1}` })
)
);
});
test.each(testSecrets)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, ...secret, key: `BULK-${secret.key}-1`, path, token: serviceToken });
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment)
}))
}
});
expect(createSharedSecRes.statusCode).toBe(400);
await deleteSecret({ path, key: `BULK-${secret.key}-1`, token: serviceToken });
});
test.each(testSecrets)("Bulk update secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path })
)
);
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, "update-value", secret.comment)
}))
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.key}-${i + 1}`,
value: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}`, token: serviceToken })
)
);
});
test.each(testSecrets)("Bulk delete secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path })
)
);
const deletedSharedSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`
}))
}
});
expect(deletedSharedSecRes.statusCode).toBe(200);
const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload);
expect(deletedSecretPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.value}-${i + 1}`,
type: SecretType.Shared
})
)
)
);
});
});
describe("Service token fail cases", async () => {
test("Unauthorized secret path access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: "/nested/deep"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(401);
expect(fetchSecrets.json().error).toBe("PermissionDenied");
await deleteServiceToken();
});
test("Unauthorized secret environment access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: "prod",
secretPath: "/"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(401);
expect(fetchSecrets.json().error).toBe("PermissionDenied");
await deleteServiceToken();
});
test("Unauthorized write operation", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read"]
);
const writeSecrets = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/NEW`,
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: SecretType.Shared,
secretPath: "/",
// doesn't matter project key because this will fail before that due to read only access
...encryptSecret(crypto.randomBytes(16).toString("hex"), "NEW", "value", "")
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(writeSecrets.statusCode).toBe(401);
expect(writeSecrets.json().error).toBe("PermissionDenied");
// but read access should still work fine
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: "/"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(200);
await deleteServiceToken();
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,30 @@
// import { main } from "@app/server/app"; // eslint-disable-next-line
import { initEnvConfig } from "@app/lib/config/env"; import "ts-node/register";
import dotenv from "dotenv"; import dotenv from "dotenv";
import jwt from "jsonwebtoken";
import knex from "knex"; import knex from "knex";
import path from "path"; import path from "path";
import { mockSmtpServer } from "./mocks/smtp";
import { initLogger } from "@app/lib/logger";
import jwt from "jsonwebtoken";
import "ts-node/register";
import { main } from "@app/server/app";
import { mockQueue } from "./mocks/queue";
import { AuthTokenType } from "@app/services/auth/auth-type";
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
import { initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger";
import { main } from "@app/server/app";
import { AuthTokenType } from "@app/services/auth/auth-type";
dotenv.config({ path: path.join(__dirname, "../.env.test") }); import { mockQueue } from "./mocks/queue";
import { mockSmtpServer } from "./mocks/smtp";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default { export default {
name: "knex-env", name: "knex-env",
transformMode: "ssr", transformMode: "ssr",
async setup() { async setup() {
const logger = await initLogger();
const cfg = initEnvConfig(logger);
const db = knex({ const db = knex({
client: "pg", client: "pg",
connection: process.env.DB_CONNECTION_URI, connection: cfg.DB_CONNECTION_URI,
migrations: { migrations: {
directory: path.join(__dirname, "../src/db/migrations"), directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts", extension: "ts",
@ -37,8 +41,6 @@ export default {
await db.seed.run(); await db.seed.run();
const smtp = mockSmtpServer(); const smtp = mockSmtpServer();
const queue = mockQueue(); const queue = mockQueue();
const logger = await initLogger();
const cfg = initEnvConfig(logger);
const server = await main({ db, smtp, logger, queue }); const server = await main({ db, smtp, logger, queue });
// @ts-expect-error type // @ts-expect-error type
globalThis.testServer = server; globalThis.testServer = server;
@ -54,6 +56,7 @@ export default {
{ expiresIn: cfg.JWT_AUTH_LIFETIME } { expiresIn: cfg.JWT_AUTH_LIFETIME }
); );
} catch (error) { } catch (error) {
console.log("[TEST] Error setting up environment", error);
await db.destroy(); await db.destroy();
throw error; throw error;
} }

2288
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,8 @@
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", "migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback", "migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts", "seed:new": "tsx ./scripts/create-seed-file.ts",
"seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest && npm run seed:run" "db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -67,21 +67,21 @@
"tsx": "^4.4.0", "tsx": "^4.4.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"vite-tsconfig-paths": "^4.2.2", "vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.0.4" "vitest": "^1.2.2"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.485.0", "@aws-sdk/client-secrets-manager": "^3.504.0",
"@casl/ability": "^6.5.0", "@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.2.0", "@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.4.1", "@fastify/cors": "^8.5.0",
"@fastify/etag": "^5.1.0", "@fastify/etag": "^5.1.0",
"@fastify/formbody": "^7.4.0", "@fastify/formbody": "^7.4.0",
"@fastify/helmet": "^11.1.1", "@fastify/helmet": "^11.1.1",
"@fastify/passport": "^2.4.0", "@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0", "@fastify/rate-limit": "^9.0.0",
"@fastify/session": "^10.7.0", "@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.12.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^1.10.1", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
@ -90,13 +90,13 @@
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1532.0", "aws-sdk": "^2.1549.0",
"axios": "^1.6.2", "axios": "^1.6.7",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bullmq": "^5.1.1", "bullmq": "^5.1.6",
"dotenv": "^16.3.1", "dotenv": "^16.4.1",
"fastify": "^4.24.3", "fastify": "^4.26.0",
"fastify-plugin": "^4.5.1", "fastify-plugin": "^4.5.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
@ -106,10 +106,11 @@
"knex": "^3.0.1", "knex": "^3.0.1",
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mysql2": "^3.6.5", "mysql2": "^3.9.1",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.9",
"ora": "^7.0.1",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0", "passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
@ -117,12 +118,12 @@
"picomatch": "^3.0.1", "picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
"posthog-node": "^3.6.0", "posthog-node": "^3.6.0",
"probot": "^12.3.3", "probot": "^13.0.0",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"zod": "^3.22.4", "zod": "^3.22.4",
"zod-to-json-schema": "^3.22.0" "zod-to-json-schema": "^3.22.4"
} }
} }

View File

@ -7,11 +7,10 @@ import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true }); const prompt = promptSync({ sigint: true });
const migrationName = prompt("Enter name for seedfile: "); const migrationName = prompt("Enter name for seedfile: ");
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1; const fileCounter = readdirSync(path.join(__dirname, "../src/db/seeds")).length || 1;
execSync( execSync(
`npx knex seed:make --knexfile ${path.join( `npx knex seed:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${
__dirname, fileCounter + 1
"../src/db/knexfile.ts" }-${migrationName}`,
)} -x ts ${fileCounter}-${migrationName}`,
{ stdio: "inherit" } { stdio: "inherit" }
); );

View File

@ -3,13 +3,9 @@ import dotenv from "dotenv";
import path from "path"; import path from "path";
import knex from "knex"; import knex from "knex";
import { writeFileSync } from "fs"; import { writeFileSync } from "fs";
import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true });
dotenv.config({ dotenv.config({
path: path.join(__dirname, "../.env"), path: path.join(__dirname, "../../.env.migration")
debug: true
}); });
const db = knex({ const db = knex({
@ -94,17 +90,7 @@ const main = async () => {
.orderBy("table_name") .orderBy("table_name")
).filter((el) => !el.tableName.includes("_migrations")); ).filter((el) => !el.tableName.includes("_migrations"));
console.log("Select a table to generate schema");
console.table(tables);
console.log("all: all tables");
const selectedTables = prompt("Type table numbers comma seperated: ");
const tableNumbers =
selectedTables !== "all" ? selectedTables.split(",").map((el) => Number(el)) : [];
for (let i = 0; i < tables.length; i += 1) { for (let i = 0; i < tables.length; i += 1) {
// skip if not desired table
if (selectedTables !== "all" && !tableNumbers.includes(i)) continue;
const { tableName } = tables[i]; const { tableName } = tables[i];
const columns = await db(tableName).columnInfo(); const columns = await db(tableName).columnInfo();
const columnNames = Object.keys(columns); const columnNames = Object.keys(columns);
@ -124,16 +110,16 @@ const main = async () => {
if (colInfo.nullable) { if (colInfo.nullable) {
ztype = ztype.concat(".nullable().optional()"); ztype = ztype.concat(".nullable().optional()");
} }
schema = schema.concat(`${!schema ? "\n" : ""} ${columnName}: ${ztype},\n`); schema = schema.concat(
`${!schema ? "\n" : ""} ${columnName}: ${ztype}${colNum === columnNames.length - 1 ? "" : ","}\n`
);
} }
const dashcase = tableName.split("_").join("-"); const dashcase = tableName.split("_").join("-");
const pascalCase = tableName const pascalCase = tableName
.split("_") .split("_")
.reduce( .reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
(prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`,
""
);
writeFileSync( writeFileSync(
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`), path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
`// Code generated by automation script, DO NOT EDIT. `// Code generated by automation script, DO NOT EDIT.
@ -152,15 +138,6 @@ export type T${pascalCase}Insert = Omit<T${pascalCase}, TImmutableDBKeys>;
export type T${pascalCase}Update = Partial<Omit<T${pascalCase}, TImmutableDBKeys>>; export type T${pascalCase}Update = Partial<Omit<T${pascalCase}, TImmutableDBKeys>>;
` `
); );
// const file = readFileSync(path.join(__dirname, "../src/db/schemas/index.ts"), "utf8");
// if (!file.includes(`export * from "./${dashcase};"`)) {
// appendFileSync(
// path.join(__dirname, "../src/db/schemas/index.ts"),
// `\nexport * from "./${dashcase}";`,
// "utf8"
// );
// }
} }
process.exit(0); process.exit(0);

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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-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 { 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 { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
@ -51,6 +52,7 @@ declare module "fastify" {
// used for mfa session authentication // used for mfa session authentication
mfa: { mfa: {
userId: string; userId: string;
orgId?: string;
user: TUsers; user: TUsers;
}; };
// identity injection. depending on which kinda of token the information is filled in auth // identity injection. depending on which kinda of token the information is filled in auth
@ -58,6 +60,7 @@ declare module "fastify" {
permission: { permission: {
type: ActorType; type: ActorType;
id: string; id: string;
orgId?: string;
}; };
// passport data // passport data
passportUser: { passportUser: {
@ -103,6 +106,7 @@ declare module "fastify" {
secretRotation: TSecretRotationServiceFactory; secretRotation: TSecretRotationServiceFactory;
snapshot: TSecretSnapshotServiceFactory; snapshot: TSecretSnapshotServiceFactory;
saml: TSamlConfigServiceFactory; saml: TSamlConfigServiceFactory;
scim: TScimServiceFactory;
auditLog: TAuditLogServiceFactory; auditLog: TAuditLogServiceFactory;
secretScanning: TSecretScanningServiceFactory; secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory; license: TLicenseServiceFactory;

View File

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

View File

@ -5,9 +5,13 @@ import dotenv from "dotenv";
import type { Knex } from "knex"; import type { Knex } from "knex";
import path from "path"; import path from "path";
// Update with your config settings. // Update with your config settings. .
dotenv.config({ dotenv.config({
path: path.join(__dirname, "../../.env"), path: path.join(__dirname, "../../../.env.migration"),
debug: true
});
dotenv.config({
path: path.join(__dirname, "../../../.env"),
debug: true debug: true
}); });
export default { export default {

View File

@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.boolean("authEnforced").defaultTo(false);
t.index("slug");
});
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
t.datetime("lastUsed");
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.dropColumn("authEnforced");
t.dropIndex("slug");
});
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
t.dropColumn("lastUsed");
});
}

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

@ -0,0 +1,20 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.SuperAdmin);
if (isTablePresent) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.string("allowedSignUpDomain");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SuperAdmin, "allowedSignUpDomain")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("allowedSignUpDomain");
});
}
}

View File

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

View File

@ -40,6 +40,7 @@ export enum TableName {
IdentityUaClientSecret = "identity_ua_client_secrets", IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityOrgMembership = "identity_org_memberships", IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembership = "identity_project_memberships",
ScimToken = "scim_tokens",
SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalRequest = "secret_approval_requests", SecretApprovalRequest = "secret_approval_requests",
@ -111,6 +112,17 @@ export enum SecretType {
Personal = "personal" 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 { export enum IdentityAuthMethod {
Univeral = "universal-auth" Univeral = "universal-auth"
} }

View File

@ -13,7 +13,9 @@ export const OrganizationsSchema = z.object({
customerId: z.string().nullable().optional(), customerId: z.string().nullable().optional(),
slug: z.string(), slug: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
authEnforced: z.boolean().default(false).nullable().optional(),
scimEnabled: z.boolean().default(false).nullable().optional()
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; 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(), autoCapitalization: z.boolean().default(true).nullable().optional(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
createdAt: z.date(), 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>; export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@ -22,7 +22,8 @@ export const SamlConfigsSchema = z.object({
certTag: z.string().nullable().optional(), certTag: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
orgId: z.string().uuid() orgId: z.string().uuid(),
lastUsed: z.date().nullable().optional()
}); });
export type TSamlConfigs = z.infer<typeof SamlConfigsSchema>; export type TSamlConfigs = z.infer<typeof SamlConfigsSchema>;

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

@ -12,7 +12,8 @@ export const SuperAdminSchema = z.object({
initialized: z.boolean().default(false).nullable().optional(), initialized: z.boolean().default(false).nullable().optional(),
allowSignUp: z.boolean().default(true).nullable().optional(), allowSignUp: z.boolean().default(true).nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
allowedSignUpDomain: z.string().nullable().optional()
}); });
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@ -19,7 +19,8 @@ export const UsersSchema = z.object({
mfaMethods: z.string().array().nullable().optional(), mfaMethods: z.string().array().nullable().optional(),
devices: z.unknown().nullable().optional(), devices: z.unknown().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
isGhost: z.boolean().default(false)
}); });
export type TUsers = z.infer<typeof UsersSchema>; 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 crypto from "node:crypto";
import argon2, { argon2id } from "argon2"; import argon2, { argon2id } from "argon2";
@ -6,17 +7,21 @@ import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
import { import {
decryptAsymmetric,
// decryptAsymmetric, // decryptAsymmetric,
decryptSymmetric, decryptSymmetric128BitHexKeyUTF8,
encryptAsymmetric, encryptAsymmetric,
encryptSymmetric encryptSymmetric128BitHexKeyUTF8
} from "@app/lib/crypto"; } from "@app/lib/crypto";
import { TUserEncryptionKeys } from "./schemas"; import { TSecrets, TUserEncryptionKeys } from "./schemas";
export let userPrivateKey: string | undefined;
export let userPublicKey: string | undefined;
export const seedData1 = { export const seedData1 = {
id: "3dafd81d-4388-432b-a4c5-f735616868c1", 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", password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
organization: { organization: {
id: "180870b7-f464-4740-8ffe-9d11c9245ea7", id: "180870b7-f464-4740-8ffe-9d11c9245ea7",
@ -31,8 +36,22 @@ export const seedData1 = {
name: "Development", name: "Development",
slug: "dev" slug: "dev"
}, },
machineIdentity: {
id: "88fa7aed-9288-401e-a4c9-fa9430be62a0",
name: "mac1",
clientCredentials: {
id: "3f6135db-f237-421d-af66-a8f4e80d443b",
secret: "da35a5a5a7b57f977a9a73394506e878a7175d06606df43dc93e1472b10cf339"
}
},
token: { token: {
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36" id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
},
// We set these values during user creation, and later re-use them during project seeding.
encryptionKeys: {
publicKey: "",
privateKey: ""
} }
}; };
@ -73,7 +92,7 @@ export const generateUserSrpKeys = async (password: string) => {
ciphertext: encryptedPrivateKey, ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag tag: encryptedPrivateKeyTag
} = encryptSymmetric(privateKey, key.toString("base64")); } = encryptSymmetric128BitHexKeyUTF8(privateKey, key);
// create the protected key by encrypting the symmetric key // create the protected key by encrypting the symmetric key
// [key] with the derived key // [key] with the derived key
@ -81,7 +100,7 @@ export const generateUserSrpKeys = async (password: string) => {
ciphertext: protectedKey, ciphertext: protectedKey,
iv: protectedKeyIV, iv: protectedKeyIV,
tag: protectedKeyTag tag: protectedKeyTag
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64")); } = encryptSymmetric128BitHexKeyUTF8(key.toString("hex"), derivedKey);
return { return {
protectedKey, protectedKey,
@ -107,32 +126,102 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
raw: true raw: true
}); });
if (!derivedKey) throw new Error("Failed to derive key from password"); if (!derivedKey) throw new Error("Failed to derive key from password");
const key = decryptSymmetric({
const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.protectedKey as string, ciphertext: user.protectedKey as string,
iv: user.protectedKeyIV as string, iv: user.protectedKeyIV as string,
tag: user.protectedKeyTag as string, tag: user.protectedKeyTag as string,
key: derivedKey.toString("base64") key: derivedKey
}); });
const privateKey = decryptSymmetric({
const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.encryptedPrivateKey, ciphertext: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
tag: user.tag, tag: user.tag,
key key: Buffer.from(key, "hex")
}); });
return privateKey; return privateKey;
}; };
export const buildUserProjectKey = async (privateKey: string, publickey: string) => { export const buildUserProjectKey = (privateKey: string, publickey: string) => {
const randomBytes = crypto.randomBytes(16).toString("hex"); const randomBytes = crypto.randomBytes(16).toString("hex");
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey); const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
return { nonce, ciphertext }; return { nonce, ciphertext };
}; };
// export const getUserProjectKey = async (privateKey: string) => { export const getUserProjectKey = async (privateKey: string, ciphertext: string, nonce: string, publicKey: string) => {
// const key = decryptAsymmetric({ return decryptAsymmetric({
// ciphertext: decryptFileKey.encryptedKey, ciphertext,
// nonce: decryptFileKey.nonce, nonce,
// publicKey: decryptFileKey.sender.publicKey, publicKey,
// privateKey: PRIVATE_KEY privateKey
// }); });
// }; };
export const encryptSecret = (encKey: string, key: string, value?: string, comment?: string) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encryptSymmetric128BitHexKeyUTF8(key, encKey);
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encryptSymmetric128BitHexKeyUTF8(value ?? "", encKey);
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encryptSymmetric128BitHexKeyUTF8(comment ?? "", encKey);
return {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
};
};
export const decryptSecret = (decryptKey: string, encSecret: TSecrets) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
key: decryptKey,
ciphertext: encSecret.secretKeyCiphertext,
tag: encSecret.secretKeyTag,
iv: encSecret.secretKeyIV
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
key: decryptKey,
ciphertext: encSecret.secretValueCiphertext,
tag: encSecret.secretValueTag,
iv: encSecret.secretValueIV
});
const secretComment =
encSecret.secretCommentIV && encSecret.secretCommentTag && encSecret.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
key: decryptKey,
ciphertext: encSecret.secretCommentCiphertext,
tag: encSecret.secretCommentTag,
iv: encSecret.secretCommentIV
})
: "";
return {
key: secretKey,
value: secretValue,
comment: secretComment,
version: encSecret.version
};
};

View File

@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
const [user] = await knex(TableName.Users) const [user] = await knex(TableName.Users)
.insert([ .insert([
{ {
// @ts-expect-error exluded type id needs to be inserted here to keep it testable // eslint-disable-next-line
// @ts-ignore
id: seedData1.id, id: seedData1.id,
email: seedData1.email, email: seedData1.email,
superAdmin: true, superAdmin: true,
@ -48,7 +49,8 @@ export async function seed(knex: Knex): Promise<void> {
]); ]);
await knex(TableName.AuthTokenSession).insert({ await knex(TableName.AuthTokenSession).insert({
// @ts-expect-error exluded type id needs to be inserted here to keep it testable // eslint-disable-next-line
// @ts-ignore
id: seedData1.token.id, id: seedData1.token.id,
userId: seedData1.id, userId: seedData1.id,
ip: "151.196.220.213", ip: "151.196.220.213",

View File

@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
const [org] = await knex(TableName.Organization) const [org] = await knex(TableName.Organization)
.insert([ .insert([
{ {
// @ts-expect-error exluded type id needs to be inserted here to keep it testable // eslint-disable-next-line
// @ts-ignore
id: seedData1.organization.id, id: seedData1.organization.id,
name: "infisical", name: "infisical",
slug: "infisical", slug: "infisical",

View File

@ -1,7 +1,11 @@
import crypto from "node:crypto";
import { Knex } from "knex"; import { Knex } from "knex";
import { OrgMembershipRole, TableName } from "../schemas"; import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { seedData1 } from "../seed-data";
import { OrgMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [ export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" }, { name: "Development", slug: "dev" },
@ -20,21 +24,32 @@ export async function seed(knex: Knex): Promise<void> {
name: seedData1.project.name, name: seedData1.project.name,
orgId: seedData1.organization.id, orgId: seedData1.organization.id,
slug: "first-project", slug: "first-project",
// @ts-expect-error exluded type id needs to be inserted here to keep it testable // eslint-disable-next-line
// @ts-ignore
id: seedData1.project.id id: seedData1.project.id
}) })
.returning("*"); .returning("*");
// await knex(TableName.ProjectKeys).insert({
// projectId: project.id,
// senderId: seedData1.id
// });
await knex(TableName.ProjectMembership).insert({ await knex(TableName.ProjectMembership).insert({
projectId: project.id, projectId: project.id,
role: OrgMembershipRole.Admin, role: OrgMembershipRole.Admin,
userId: seedData1.id userId: seedData1.id
}); });
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
if (!user) throw new Error("User not found");
const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
await knex(TableName.ProjectKeys).insert({
projectId: project.id,
nonce: projectKey.nonce,
encryptedKey: projectKey.ciphertext,
receiverId: seedData1.id,
senderId: seedData1.id
});
// create default environments and default folders
const envs = await knex(TableName.Environment) const envs = await knex(TableName.Environment)
.insert( .insert(
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({ DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
@ -46,4 +61,19 @@ export async function seed(knex: Knex): Promise<void> {
) )
.returning("*"); .returning("*");
await knex(TableName.SecretFolder).insert(envs.map(({ id }) => ({ name: "root", envId: id, parentId: null }))); await knex(TableName.SecretFolder).insert(envs.map(({ id }) => ({ name: "root", envId: id, parentId: null })));
// save secret secret blind index
const encKey = process.env.ENCRYPTION_KEY;
if (!encKey) throw new Error("Missing ENCRYPTION_KEY");
const salt = crypto.randomBytes(16).toString("base64");
const secretBlindIndex = encryptSymmetric128BitHexKeyUTF8(salt, encKey);
// insert secret blind index for project
await knex(TableName.SecretBlindIndex).insert({
projectId: project.id,
encryptedSaltCipherText: secretBlindIndex.ciphertext,
saltIV: secretBlindIndex.iv,
saltTag: secretBlindIndex.tag,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
});
} }

View File

@ -0,0 +1,83 @@
import bcrypt from "bcrypt";
import { Knex } from "knex";
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries
await knex(TableName.Identity).del();
await knex(TableName.IdentityOrgMembership).del();
// Inserts seed entries
await knex(TableName.Identity).insert([
{
// eslint-disable-next-line
// @ts-ignore
id: seedData1.machineIdentity.id,
name: seedData1.machineIdentity.name,
authMethod: IdentityAuthMethod.Univeral
}
]);
const identityUa = await knex(TableName.IdentityUniversalAuth)
.insert([
{
identityId: seedData1.machineIdentity.id,
clientId: seedData1.machineIdentity.clientCredentials.id,
clientSecretTrustedIps: JSON.stringify([
{
type: "ipv4",
prefix: 0,
ipAddress: "0.0.0.0"
},
{
type: "ipv6",
prefix: 0,
ipAddress: "::"
}
]),
accessTokenTrustedIps: JSON.stringify([
{
type: "ipv4",
prefix: 0,
ipAddress: "0.0.0.0"
},
{
type: "ipv6",
prefix: 0,
ipAddress: "::"
}
]),
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0
}
])
.returning("*");
const clientSecretHash = await bcrypt.hash(seedData1.machineIdentity.clientCredentials.secret, 10);
await knex(TableName.IdentityUaClientSecret).insert([
{
identityUAId: identityUa[0].id,
description: "",
clientSecretTTL: 0,
clientSecretNumUses: 0,
clientSecretNumUsesLimit: 0,
clientSecretPrefix: seedData1.machineIdentity.clientCredentials.secret.slice(0, 4),
clientSecretHash,
isClientSecretRevoked: false
}
]);
await knex(TableName.IdentityOrgMembership).insert([
{
identityId: seedData1.machineIdentity.id,
orgId: seedData1.organization.id,
role: OrgMembershipRole.Admin
}
]);
await knex(TableName.IdentityProjectMembership).insert({
identityId: seedData1.machineIdentity.id,
role: ProjectMembershipRole.Admin,
projectId: seedData1.project.id
});
}

View File

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

View File

@ -22,6 +22,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgPlansTableByBillCycle({ const data = await server.services.license.getOrgPlansTableByBillCycle({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId, orgId: req.params.organizationId,
billingCycle: req.query.billingCycle billingCycle: req.query.billingCycle
}); });
@ -43,6 +44,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const plan = await server.services.license.getOrgPlan({ const plan = await server.services.license.getOrgPlan({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return { plan }; return { plan };
@ -85,6 +87,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.startOrgTrial({ const data = await server.services.license.startOrgTrial({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId, orgId: req.params.organizationId,
success_url: req.body.success_url success_url: req.body.success_url
}); });
@ -106,6 +109,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.createOrganizationPortalSession({ const data = await server.services.license.createOrganizationPortalSession({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return data; return data;
@ -126,6 +130,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgBillingInfo({ const data = await server.services.license.getOrgBillingInfo({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return data; return data;
@ -146,6 +151,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgPlanTable({ const data = await server.services.license.getOrgPlanTable({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return data; return data;
@ -166,6 +172,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgBillingDetails({ const data = await server.services.license.getOrgBillingDetails({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return data; return data;
@ -190,6 +197,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.updateOrgBillingDetails({ const data = await server.services.license.updateOrgBillingDetails({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId, orgId: req.params.organizationId,
name: req.body.name, name: req.body.name,
email: req.body.email email: req.body.email
@ -212,6 +220,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgPmtMethods({ const data = await server.services.license.getOrgPmtMethods({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return data; return data;
@ -236,6 +245,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.addOrgPmtMethods({ const data = await server.services.license.addOrgPmtMethods({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId, orgId: req.params.organizationId,
success_url: req.body.success_url, success_url: req.body.success_url,
cancel_url: req.body.cancel_url cancel_url: req.body.cancel_url
@ -261,6 +271,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.delOrgPmtMethods({ const data = await server.services.license.delOrgPmtMethods({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId, orgId: req.params.organizationId,
pmtMethodId: req.params.pmtMethodId pmtMethodId: req.params.pmtMethodId
}); });
@ -284,6 +295,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgTaxIds({ const data = await server.services.license.getOrgTaxIds({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return data; return data;
@ -310,6 +322,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.addOrgTaxId({ const data = await server.services.license.addOrgTaxId({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId, orgId: req.params.organizationId,
type: req.body.type, type: req.body.type,
value: req.body.value value: req.body.value
@ -335,6 +348,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.delOrgTaxId({ const data = await server.services.license.delOrgTaxId({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId, orgId: req.params.organizationId,
taxId: req.params.taxId taxId: req.params.taxId
}); });
@ -358,6 +372,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgTaxInvoices({ const data = await server.services.license.getOrgTaxInvoices({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return data; return data;
@ -380,6 +395,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgLicenses({ const data = await server.services.license.getOrgLicenses({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return data; return data;

View File

@ -26,7 +26,12 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const role = await server.services.orgRole.createRole(req.permission.id, req.params.organizationId, req.body); const role = await server.services.orgRole.createRole(
req.permission.id,
req.params.organizationId,
req.body,
req.permission.orgId
);
return { role }; return { role };
} }
}); });
@ -57,7 +62,8 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.id, req.permission.id,
req.params.organizationId, req.params.organizationId,
req.params.roleId, req.params.roleId,
req.body req.body,
req.permission.orgId
); );
return { role }; return { role };
} }
@ -82,7 +88,8 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
const role = await server.services.orgRole.deleteRole( const role = await server.services.orgRole.deleteRole(
req.permission.id, req.permission.id,
req.params.organizationId, req.params.organizationId,
req.params.roleId req.params.roleId,
req.permission.orgId
); );
return { role }; return { role };
} }
@ -107,7 +114,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const roles = await server.services.orgRole.listRoles(req.permission.id, req.params.organizationId); const roles = await server.services.orgRole.listRoles(
req.permission.id,
req.params.organizationId,
req.permission.orgId
);
return { data: { roles } }; return { data: { roles } };
} }
}); });
@ -130,7 +141,8 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const { permissions, membership } = await server.services.orgRole.getUserPermission( const { permissions, membership } = await server.services.orgRole.getUserPermission(
req.permission.id, req.permission.id,
req.params.organizationId req.params.organizationId,
req.permission.orgId
); );
return { permissions, membership }; return { permissions, membership };
} }

View File

@ -30,7 +30,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.permission.type, req.permission.type,
req.permission.id, req.permission.id,
req.params.projectId, req.params.projectId,
req.body req.body,
req.permission.orgId
); );
return { role }; return { role };
} }
@ -63,7 +64,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.permission.id, req.permission.id,
req.params.projectId, req.params.projectId,
req.params.roleId, req.params.roleId,
req.body req.body,
req.permission.orgId
); );
return { role }; return { role };
} }
@ -89,7 +91,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.permission.type, req.permission.type,
req.permission.id, req.permission.id,
req.params.projectId, req.params.projectId,
req.params.roleId req.params.roleId,
req.permission.orgId
); );
return { role }; return { role };
} }
@ -117,7 +120,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
const roles = await server.services.projectRole.listRoles( const roles = await server.services.projectRole.listRoles(
req.permission.type, req.permission.type,
req.permission.id, req.permission.id,
req.params.projectId req.params.projectId,
req.permission.orgId
); );
return { data: { roles } }; return { data: { roles } };
} }
@ -143,7 +147,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const { permissions, membership } = await server.services.projectRole.getUserPermission( const { permissions, membership } = await server.services.projectRole.getUserPermission(
req.permission.id, req.permission.id,
req.params.projectId req.params.projectId,
req.permission.orgId
); );
return { data: { permissions, membership } }; return { data: { permissions, membership } };
} }

View File

@ -11,6 +11,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "GET", method: "GET",
url: "/:workspaceId/secret-snapshots", url: "/:workspaceId/secret-snapshots",
schema: { schema: {
description: "Return project secret snapshots ids",
security: [
{
apiKeyAuth: [],
bearerAuth: []
}
],
params: z.object({ params: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim()
}), }),
@ -31,6 +38,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const secretSnapshots = await server.services.snapshot.listSnapshots({ const secretSnapshots = await server.services.snapshot.listSnapshots({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
...req.query ...req.query
}); });
@ -60,6 +68,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const count = await server.services.snapshot.projectSecretSnapshotCount({ const count = await server.services.snapshot.projectSecretSnapshotCount({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
environment: req.query.environment, environment: req.query.environment,
path: req.query.path path: req.query.path
@ -72,6 +81,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "GET", method: "GET",
url: "/:workspaceId/audit-logs", url: "/:workspaceId/audit-logs",
schema: { schema: {
description: "Return audit logs",
security: [
{
bearerAuth: [],
apiKeyAuth: []
}
],
params: z.object({ params: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim()
}), }),
@ -112,6 +128,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const auditLogs = await server.services.auditLog.listProjectAuditLogs({ const auditLogs = await server.services.auditLog.listProjectAuditLogs({
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
...req.query, ...req.query,
auditLogActor: req.query.actor, auditLogActor: req.query.actor,

View File

@ -13,13 +13,12 @@ import { FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { SamlConfigsSchema } from "@app/db/schemas"; import { SamlConfigsSchema } from "@app/db/schemas";
import { SamlProviders } from "@app/ee/services/saml-config/saml-config-types"; import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
type TSAMLConfig = { type TSAMLConfig = {
callbackUrl: string; callbackUrl: string;
@ -28,6 +27,7 @@ type TSAMLConfig = {
cert: string; cert: string;
audience: string; audience: string;
wantAuthnResponseSigned?: boolean; wantAuthnResponseSigned?: boolean;
disableRequestedAuthnContext?: boolean;
}; };
export const registerSamlRouter = async (server: FastifyZodProvider) => { export const registerSamlRouter = async (server: FastifyZodProvider) => {
@ -44,17 +44,30 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line // eslint-disable-next-line
getSamlOptions: async (req, done) => { getSamlOptions: async (req, done) => {
try { try {
const { ssoIdentifier } = req.params; const { samlConfigId, orgSlug } = req.params;
if (!ssoIdentifier) throw new BadRequestError({ message: "Missing sso identitier" });
const ssoConfig = await server.services.saml.getSaml({ let ssoLookupDetails: TGetSamlCfgDTO;
if (orgSlug) {
ssoLookupDetails = {
type: "orgSlug",
orgSlug
};
} else if (samlConfigId) {
ssoLookupDetails = {
type: "ssoId", type: "ssoId",
id: ssoIdentifier id: samlConfigId
}); };
if (!ssoConfig) throw new BadRequestError({ message: "SSO config not found" }); } else {
throw new BadRequestError({ message: "Missing sso identitier or org slug" });
}
const ssoConfig = await server.services.saml.getSaml(ssoLookupDetails);
if (!ssoConfig || !ssoConfig.isActive)
throw new BadRequestError({ message: "Failed to authenticate with SAML SSO" });
const samlConfig: TSAMLConfig = { const samlConfig: TSAMLConfig = {
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoIdentifier}`, callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`,
entryPoint: ssoConfig.entryPoint, entryPoint: ssoConfig.entryPoint,
issuer: ssoConfig.issuer, issuer: ssoConfig.issuer,
cert: ssoConfig.cert, cert: ssoConfig.cert,
@ -64,7 +77,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
samlConfig.wantAuthnResponseSigned = false; samlConfig.wantAuthnResponseSigned = false;
} }
if (ssoConfig.authProvider === SamlProviders.AZURE_SAML) { if (ssoConfig.authProvider === SamlProviders.AZURE_SAML) {
if (req.body.RelayState && JSON.parse(req.body.RelayState).spIntiaited) { samlConfig.disableRequestedAuthnContext = true;
if (req.body?.RelayState && JSON.parse(req.body.RelayState).spInitiated) {
samlConfig.audience = `spn:${ssoConfig.issuer}`; samlConfig.audience = `spn:${ssoConfig.issuer}`;
} }
} }
@ -79,7 +93,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line // eslint-disable-next-line
async (req, profile, cb) => { async (req, profile, cb) => {
try { try {
const serverCfg = await getServerCfg();
if (!profile) throw new BadRequestError({ message: "Missing profile" }); if (!profile) throw new BadRequestError({ message: "Missing profile" });
const { firstName } = profile; const { firstName } = profile;
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
@ -92,7 +105,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
email, email,
firstName: profile.firstName as string, firstName: profile.firstName as string,
lastName: profile.lastName as string, lastName: profile.lastName as string,
isSignupAllowed: Boolean(serverCfg.allowSignUp),
relayState: (req.body as { RelayState?: string }).RelayState, relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
@ -108,11 +120,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
); );
server.route({ server.route({
url: "/redirect/saml2/:ssoIdentifier", url: "/redirect/saml2/organizations/:orgSlug",
method: "GET", method: "GET",
schema: { schema: {
params: z.object({ params: z.object({
ssoIdentifier: z.string().trim() orgSlug: z.string().trim()
}), }),
querystring: z.object({ querystring: z.object({
callback_port: z.string().optional() callback_port: z.string().optional()
@ -134,11 +146,37 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
}); });
server.route({ server.route({
url: "/saml2/:ssoIdentifier", url: "/redirect/saml2/:samlConfigId",
method: "GET",
schema: {
params: z.object({
samlConfigId: z.string().trim()
}),
querystring: z.object({
callback_port: z.string().optional()
})
},
preValidation: (req, res) =>
(
passport.authenticate("saml", {
failureRedirect: "/",
additionalParams: {
RelayState: JSON.stringify({
spInitiated: true,
callbackPort: req.query.callback_port ?? ""
})
}
} as any) as any
)(req, res),
handler: () => {}
});
server.route({
url: "/saml2/:samlConfigId",
method: "POST", method: "POST",
schema: { schema: {
params: z.object({ params: z.object({
ssoIdentifier: z.string().trim() samlConfigId: z.string().trim()
}) })
}, },
preValidation: passport.authenticate("saml", { preValidation: passport.authenticate("saml", {
@ -177,7 +215,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
isActive: z.boolean(), isActive: z.boolean(),
entryPoint: z.string(), entryPoint: z.string(),
issuer: z.string(), issuer: z.string(),
cert: z.string() cert: z.string(),
lastUsed: z.date().nullable().optional()
}) })
.optional() .optional()
} }
@ -186,6 +225,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
const saml = await server.services.saml.getSaml({ const saml = await server.services.saml.getSaml({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.query.organizationId, orgId: req.query.organizationId,
type: "org" type: "org"
}); });
@ -214,6 +254,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
const saml = await server.services.saml.createSamlCfg({ const saml = await server.services.saml.createSamlCfg({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId, orgId: req.body.organizationId,
...req.body ...req.body
}); });
@ -244,6 +285,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
const saml = await server.services.saml.updateSamlCfg({ const saml = await server.services.saml.updateSamlCfg({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId, orgId: req.body.organizationId,
...req.body ...req.body
}); });

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" }, (_, 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

@ -34,6 +34,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
...req.body, ...req.body,
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
@ -71,6 +72,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
...req.body, ...req.body,
secretPolicyId: req.params.sapId secretPolicyId: req.params.sapId
}); });
@ -96,6 +98,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
secretPolicyId: req.params.sapId secretPolicyId: req.params.sapId
}); });
return { approval }; return { approval };
@ -120,6 +123,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({ const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId projectId: req.query.workspaceId
}); });
return { approvals }; return { approvals };
@ -146,6 +150,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId, projectId: req.query.workspaceId,
...req.query ...req.query
}); });

View File

@ -52,6 +52,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({ const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
...req.query, ...req.query,
projectId: req.query.workspaceId projectId: req.query.workspaceId
}); });
@ -80,6 +81,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const approvals = await server.services.secretApprovalRequest.requestCount({ const approvals = await server.services.secretApprovalRequest.requestCount({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId projectId: req.query.workspaceId
}); });
return { approvals }; return { approvals };
@ -104,6 +106,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({ const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
approvalId: req.params.id approvalId: req.params.id
}); });
return { approval }; return { approval };
@ -131,6 +134,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const review = await server.services.secretApprovalRequest.reviewApproval({ const review = await server.services.secretApprovalRequest.reviewApproval({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
approvalId: req.params.id, approvalId: req.params.id,
status: req.body.status status: req.body.status
}); });
@ -159,6 +163,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const approval = await server.services.secretApprovalRequest.updateApprovalStatus({ const approval = await server.services.secretApprovalRequest.updateApprovalStatus({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId,
approvalId: req.params.id, approvalId: req.params.id,
status: req.body.status status: req.body.status
}); });
@ -266,6 +271,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const approval = await server.services.secretApprovalRequest.getSecretApprovalDetails({ const approval = await server.services.secretApprovalRequest.getSecretApprovalDetails({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
id: req.params.id id: req.params.id
}); });
return { approval }; return { approval };

View File

@ -30,6 +30,7 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro
const providers = await server.services.secretRotation.getProviderTemplates({ const providers = await server.services.secretRotation.getProviderTemplates({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId projectId: req.params.workspaceId
}); });
return providers; return providers;

View File

@ -40,6 +40,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
const secretRotation = await server.services.secretRotation.createRotation({ const secretRotation = await server.services.secretRotation.createRotation({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectId: req.body.workspaceId projectId: req.body.workspaceId
}); });
@ -73,6 +74,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
const secretRotation = await server.services.secretRotation.restartById({ const secretRotation = await server.services.secretRotation.restartById({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
rotationId: req.body.id rotationId: req.body.id
}); });
return { secretRotation }; return { secretRotation };
@ -123,6 +125,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
const secretRotations = await server.services.secretRotation.getByProjectId({ const secretRotations = await server.services.secretRotation.getByProjectId({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId projectId: req.query.workspaceId
}); });
return { secretRotations }; return { secretRotations };
@ -155,6 +158,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
const secretRotation = await server.services.secretRotation.deleteById({ const secretRotation = await server.services.secretRotation.deleteById({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
rotationId: req.params.id rotationId: req.params.id
}); });
return { secretRotation }; return { secretRotation };

View File

@ -22,6 +22,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const session = await server.services.secretScanning.createInstallationSession({ const session = await server.services.secretScanning.createInstallationSession({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId orgId: req.body.organizationId
}); });
return session; return session;
@ -45,6 +46,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const { installatedApp } = await server.services.secretScanning.linkInstallationToOrg({ const { installatedApp } = await server.services.secretScanning.linkInstallationToOrg({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
...req.body ...req.body
}); });
return installatedApp; return installatedApp;
@ -65,6 +67,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const appInstallationCompleted = await server.services.secretScanning.getOrgInstallationStatus({ const appInstallationCompleted = await server.services.secretScanning.getOrgInstallationStatus({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return { appInstallationCompleted }; return { appInstallationCompleted };
@ -85,6 +88,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const { risks } = await server.services.secretScanning.getRisksByOrg({ const { risks } = await server.services.secretScanning.getRisksByOrg({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId orgId: req.params.organizationId
}); });
return { risks }; return { risks };
@ -106,6 +110,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const { risk } = await server.services.secretScanning.updateRiskStatus({ const { risk } = await server.services.secretScanning.updateRiskStatus({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId, orgId: req.params.organizationId,
riskId: req.params.riskId, riskId: req.params.riskId,
...req.body ...req.body

View File

@ -27,6 +27,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
const secretVersions = await server.services.secret.getSecretVersions({ const secretVersions = await server.services.secret.getSecretVersions({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
limit: req.query.limit, limit: req.query.limit,
offset: req.query.offset, offset: req.query.offset,
secretId: req.params.secretId secretId: req.params.secretId

View File

@ -46,6 +46,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
const secretSnapshot = await server.services.snapshot.getSnapshotData({ const secretSnapshot = await server.services.snapshot.getSnapshotData({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId id: req.params.secretSnapshotId
}); });
return { secretSnapshot }; return { secretSnapshot };
@ -56,6 +57,13 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/:secretSnapshotId/rollback", url: "/:secretSnapshotId/rollback",
schema: { schema: {
description: "Roll back project secrets to those captured in a secret snapshot version.",
security: [
{
apiKeyAuth: [],
bearerAuth: []
}
],
params: z.object({ params: z.object({
secretSnapshotId: z.string().trim() secretSnapshotId: z.string().trim()
}), }),
@ -70,6 +78,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
const secretSnapshot = await server.services.snapshot.rollbackSnapshot({ const secretSnapshot = await server.services.snapshot.rollbackSnapshot({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId id: req.params.secretSnapshotId
}); });
return { secretSnapshot }; return { secretSnapshot };

View File

@ -24,7 +24,8 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
const trustedIps = await server.services.trustedIp.listIpsByProjectId({ const trustedIps = await server.services.trustedIp.listIpsByProjectId({
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id actorId: req.permission.id,
actorOrgId: req.permission.orgId
}); });
return { trustedIps }; return { trustedIps };
} }
@ -54,6 +55,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
...req.body ...req.body
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@ -97,6 +99,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
trustedIpId: req.params.trustedIpId, trustedIpId: req.params.trustedIpId,
...req.body ...req.body
}); });
@ -137,6 +140,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId,
trustedIpId: req.params.trustedIpId trustedIpId: req.params.trustedIpId
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({

View File

@ -30,10 +30,11 @@ export const auditLogServiceFactory = ({
startDate, startDate,
actor, actor,
actorId, actorId,
actorOrgId,
projectId, projectId,
auditLogActor auditLogActor
}: TListProjectAuditLogDTO) => { }: TListProjectAuditLogDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
const auditLogs = await auditLogDAL.find({ const auditLogs = await auditLogDAL.find({
startDate, startDate,
@ -57,6 +58,7 @@ export const auditLogServiceFactory = ({
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { 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" }); if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
} }
return auditLogQueue.pushToLog(data); return auditLogQueue.pushToLog(data);
}; };

View File

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

View File

@ -0,0 +1,27 @@
export const getDefaultOnPremFeatures = () => {
return {
_id: null,
slug: null,
tier: -1,
workspaceLimit: null,
workspacesUsed: 0,
memberLimit: null,
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: true,
rbac: false,
customRateLimits: false,
customAlerts: false,
auditLogs: false,
auditLogsRetentionDays: 0,
samlSSO: false,
status: null,
trial_end: null,
has_used_trial: true,
secretApproval: false,
secretRotation: true
};
};

View File

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

View File

@ -44,7 +44,7 @@ type TLicenseServiceFactoryDep = {
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>; export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login"; const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/licence-login"; const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`; const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`;
export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: TLicenseServiceFactoryDep) => { export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: TLicenseServiceFactoryDep) => {
@ -92,7 +92,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
// else it would reach catch statement // else it would reach catch statement
isValidLicense = true; isValidLicense = true;
} catch (error) { } catch (error) {
logger.error(`init-license: encountered an error when init license [error]`, error); logger.error(error, `init-license: encountered an error when init license`);
} }
}; };
@ -175,8 +175,14 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
}; };
// below all are api calls // below all are api calls
const getOrgPlansTableByBillCycle = async ({ orgId, actor, actorId, billingCycle }: TOrgPlansTableDTO) => { const getOrgPlansTableByBillCycle = async ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); orgId,
actor,
actorId,
actorOrgId,
billingCycle
}: TOrgPlansTableDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const { data } = await licenseServerCloudApi.request.get( const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` `/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
@ -184,15 +190,15 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return data; return data;
}; };
const getOrgPlan = async ({ orgId, actor, actorId, projectId }: TOrgPlanDTO) => { const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, projectId }: TOrgPlanDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const plan = await getPlan(orgId, projectId); const plan = await getPlan(orgId, projectId);
return plan; return plan;
}; };
const startOrgTrial = async ({ orgId, actorId, actor, success_url }: TStartOrgTrialDTO) => { const startOrgTrial = async ({ orgId, actorId, actor, actorOrgId, success_url }: TStartOrgTrialDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
@ -213,8 +219,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return { url }; return { url };
}; };
const createOrganizationPortalSession = async ({ orgId, actorId, actor }: TCreateOrgPortalSession) => { const createOrganizationPortalSession = async ({ orgId, actorId, actor, actorOrgId }: TCreateOrgPortalSession) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
@ -260,8 +266,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return { url }; return { url };
}; };
const getOrgBillingInfo = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => { const getOrgBillingInfo = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -277,8 +283,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
}; };
// returns org current plan feature table // returns org current plan feature table
const getOrgPlanTable = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => { const getOrgPlanTable = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -293,8 +299,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return data; return data;
}; };
const getOrgBillingDetails = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => { const getOrgBillingDetails = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -310,8 +316,15 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return data; return data;
}; };
const updateOrgBillingDetails = async ({ actorId, actor, orgId, name, email }: TUpdateOrgBillingDetailsDTO) => { const updateOrgBillingDetails = async ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); actorId,
actor,
actorOrgId,
orgId,
name,
email
}: TUpdateOrgBillingDetailsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -330,8 +343,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return data; return data;
}; };
const getOrgPmtMethods = async ({ orgId, actor, actorId }: TOrgPmtMethodsDTO) => { const getOrgPmtMethods = async ({ orgId, actor, actorId, actorOrgId }: TOrgPmtMethodsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -349,8 +362,15 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return pmtMethods; return pmtMethods;
}; };
const addOrgPmtMethods = async ({ orgId, actor, actorId, success_url, cancel_url }: TAddOrgPmtMethodDTO) => { const addOrgPmtMethods = async ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); orgId,
actor,
actorId,
actorOrgId,
success_url,
cancel_url
}: TAddOrgPmtMethodDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -371,8 +391,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return { url }; return { url };
}; };
const delOrgPmtMethods = async ({ actorId, actor, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => { const delOrgPmtMethods = async ({ actorId, actor, actorOrgId, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -388,8 +408,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return data; return data;
}; };
const getOrgTaxIds = async ({ orgId, actor, actorId }: TGetOrgTaxIdDTO) => { const getOrgTaxIds = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -406,8 +426,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return taxIds; return taxIds;
}; };
const addOrgTaxId = async ({ actorId, actor, orgId, type, value }: TAddOrgTaxIdDTO) => { const addOrgTaxId = async ({ actorId, actor, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -427,8 +447,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return data; return data;
}; };
const delOrgTaxId = async ({ orgId, actor, actorId, taxId }: TDelOrgTaxIdDTO) => { const delOrgTaxId = async ({ orgId, actor, actorId, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -444,8 +464,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return data; return data;
}; };
const getOrgTaxInvoices = async ({ actorId, actor, orgId }: TOrgInvoiceDTO) => { const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, orgId }: TOrgInvoiceDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@ -461,8 +481,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
return invoices; return invoices;
}; };
const getOrgLicenses = async ({ orgId, actor, actorId }: TOrgLicensesDTO) => { const getOrgLicenses = async ({ orgId, actor, actorId, actorOrgId }: TOrgLicensesDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);

View File

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

View File

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

View File

@ -10,8 +10,10 @@ export const permissionDALFactory = (db: TDbClient) => {
try { try {
const membership = await db(TableName.OrgMembership) const membership = await db(TableName.OrgMembership)
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`) .leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
.where("userId", userId) .where("userId", userId)
.where(`${TableName.OrgMembership}.orgId`, orgId) .where(`${TableName.OrgMembership}.orgId`, orgId)
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
.select("permissions") .select("permissions")
.select(selectAllTableCols(TableName.OrgMembership)) .select(selectAllTableCols(TableName.OrgMembership))
.first(); .first();
@ -26,9 +28,11 @@ export const permissionDALFactory = (db: TDbClient) => {
try { try {
const membership = await db(TableName.IdentityOrgMembership) const membership = await db(TableName.IdentityOrgMembership)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`)
.where("identityId", identityId) .where("identityId", identityId)
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId) .where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
.select(selectAllTableCols(TableName.IdentityOrgMembership)) .select(selectAllTableCols(TableName.IdentityOrgMembership))
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
.select("permissions") .select("permissions")
.first(); .first();
return membership; return membership;
@ -41,9 +45,15 @@ export const permissionDALFactory = (db: TDbClient) => {
try { try {
const membership = await db(TableName.ProjectMembership) const membership = await db(TableName.ProjectMembership)
.leftJoin(TableName.ProjectRoles, `${TableName.ProjectMembership}.roleId`, `${TableName.ProjectRoles}.id`) .leftJoin(TableName.ProjectRoles, `${TableName.ProjectMembership}.roleId`, `${TableName.ProjectRoles}.id`)
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.where("userId", userId) .where("userId", userId)
.where(`${TableName.ProjectMembership}.projectId`, projectId) .where(`${TableName.ProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.ProjectMembership)) .select(selectAllTableCols(TableName.ProjectMembership))
.select(
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project)
)
.select("permissions") .select("permissions")
.first(); .first();

View File

@ -94,12 +94,15 @@ export const permissionServiceFactory = ({
/* /*
* Get user permission in an organization * Get user permission in an organization
* */ * */
const getUserOrgPermission = async (userId: string, orgId: string) => { const getUserOrgPermission = async (userId: string, orgId: string, userOrgId?: string) => {
const membership = await permissionDAL.getOrgPermission(userId, orgId); const membership = await permissionDAL.getOrgPermission(userId, orgId);
if (!membership) throw new UnauthorizedError({ name: "User not in org" }); if (!membership) throw new UnauthorizedError({ name: "User not in org" });
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new BadRequestError({ name: "Custom permission not found" });
} }
if (membership.orgAuthEnforced && membership.orgId !== userOrgId) {
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
}
return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
}; };
@ -112,10 +115,10 @@ export const permissionServiceFactory = ({
return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
}; };
const getOrgPermission = async (type: ActorType, id: string, orgId: string) => { const getOrgPermission = async (type: ActorType, id: string, orgId: string, actorOrgId?: string) => {
switch (type) { switch (type) {
case ActorType.USER: case ActorType.USER:
return getUserOrgPermission(id, orgId); return getUserOrgPermission(id, orgId, actorOrgId);
case ActorType.IDENTITY: case ActorType.IDENTITY:
return getIdentityOrgPermission(id, orgId); return getIdentityOrgPermission(id, orgId);
default: default:
@ -142,12 +145,17 @@ export const permissionServiceFactory = ({
}; };
// user permission for a project in an organization // user permission for a project in an organization
const getUserProjectPermission = async (userId: string, projectId: string) => { const getUserProjectPermission = async (userId: string, projectId: string, userOrgId?: string) => {
const membership = await permissionDAL.getProjectPermission(userId, projectId); const membership = await permissionDAL.getProjectPermission(userId, projectId);
if (!membership) throw new UnauthorizedError({ name: "User not in project" }); if (!membership) throw new UnauthorizedError({ name: "User not in project" });
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) { if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new BadRequestError({ name: "Custom permission not found" });
} }
if (membership.orgAuthEnforced && membership.orgId !== userOrgId) {
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
}
return { return {
permission: buildProjectPermission(membership.role, membership.permissions), permission: buildProjectPermission(membership.role, membership.permissions),
membership membership
@ -160,6 +168,7 @@ export const permissionServiceFactory = ({
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) { if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new BadRequestError({ name: "Custom permission not found" });
} }
return { return {
permission: buildProjectPermission(membership.role, membership.permissions), permission: buildProjectPermission(membership.role, membership.permissions),
membership membership
@ -168,6 +177,8 @@ export const permissionServiceFactory = ({
const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => { const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => {
const serviceToken = await serviceTokenDAL.findById(serviceTokenId); const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
if (!serviceToken) throw new BadRequestError({ message: "Service token not found" });
if (serviceToken.projectId !== projectId) if (serviceToken.projectId !== projectId)
throw new UnauthorizedError({ throw new UnauthorizedError({
message: "Failed to find service authorization for given project" message: "Failed to find service authorization for given project"
@ -184,6 +195,8 @@ export const permissionServiceFactory = ({
: { : {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>; permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & { membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & {
orgAuthEnforced: boolean;
orgId: string;
permissions?: unknown; permissions?: unknown;
}; };
}; };
@ -191,11 +204,12 @@ export const permissionServiceFactory = ({
const getProjectPermission = async <T extends ActorType>( const getProjectPermission = async <T extends ActorType>(
type: T, type: T,
id: string, id: string,
projectId: string projectId: string,
actorOrgId?: string
): Promise<TProjectPermissionRT<T>> => { ): Promise<TProjectPermissionRT<T>> => {
switch (type) { switch (type) {
case ActorType.USER: case ActorType.USER:
return getUserProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>; return getUserProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
case ActorType.SERVICE: case ActorType.SERVICE:
return getServiceTokenProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>; return getServiceTokenProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>;
case ActorType.IDENTITY: case ActorType.IDENTITY:

View File

@ -1,10 +1,31 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>; export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
export const samlConfigDALFactory = (db: TDbClient) => { export const samlConfigDALFactory = (db: TDbClient) => {
const samlCfgOrm = ormify(db, TableName.SamlConfig); const samlCfgOrm = ormify(db, TableName.SamlConfig);
return samlCfgOrm;
const findEnforceableSamlCfg = async (orgId: string) => {
try {
const samlCfg = await db(TableName.SamlConfig)
.where({
orgId,
isActive: true
})
.whereNotNull("lastUsed")
.first();
return samlCfg;
} catch (error) {
throw new DatabaseError({ error, name: "Find org by id" });
}
};
return {
...samlCfgOrm,
findEnforceableSamlCfg
};
}; };

View File

@ -18,7 +18,7 @@ import {
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@ -27,18 +27,15 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TSamlConfigDALFactory } from "./saml-config-dal"; import { TSamlConfigDALFactory } from "./saml-config-dal";
import { import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types";
SamlProviders,
TCreateSamlCfgDTO,
TGetSamlCfgDTO,
TSamlLoginDTO,
TUpdateSamlCfgDTO
} from "./saml-config-types";
type TSamlConfigServiceFactoryDep = { type TSamlConfigServiceFactoryDep = {
samlConfigDAL: TSamlConfigDALFactory; samlConfigDAL: TSamlConfigDALFactory;
userDAL: Pick<TUserDALFactory, "create" | "findUserByEmail" | "transaction" | "updateById">; userDAL: Pick<TUserDALFactory, "create" | "findUserByEmail" | "transaction" | "updateById">;
orgDAL: Pick<TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById">; orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@ -57,6 +54,7 @@ export const samlConfigServiceFactory = ({
const createSamlCfg = async ({ const createSamlCfg = async ({
cert, cert,
actor, actor,
actorOrgId,
orgId, orgId,
issuer, issuer,
actorId, actorId,
@ -64,7 +62,7 @@ export const samlConfigServiceFactory = ({
entryPoint, entryPoint,
authProvider authProvider
}: TCreateSamlCfgDTO) => { }: TCreateSamlCfgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@ -140,12 +138,14 @@ export const samlConfigServiceFactory = ({
certIV, certIV,
certTag certTag
}); });
return samlConfig; return samlConfig;
}; };
const updateSamlCfg = async ({ const updateSamlCfg = async ({
orgId, orgId,
actor, actor,
actorOrgId,
cert, cert,
actorId, actorId,
issuer, issuer,
@ -153,7 +153,7 @@ export const samlConfigServiceFactory = ({
entryPoint, entryPoint,
authProvider authProvider
}: TUpdateSamlCfgDTO) => { }: TUpdateSamlCfgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (!plan.samlSSO) if (!plan.samlSSO)
@ -162,7 +162,7 @@ export const samlConfigServiceFactory = ({
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
}); });
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive }; const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null };
const orgBot = await orgBotDAL.findOne({ orgId }); const orgBot = await orgBotDAL.findOne({ orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({ const key = infisicalSymmetricDecrypt({
@ -195,6 +195,8 @@ export const samlConfigServiceFactory = ({
updateQuery.certTag = certTag; updateQuery.certTag = certTag;
} }
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false });
return ssoConfig; return ssoConfig;
}; };
@ -203,6 +205,10 @@ export const samlConfigServiceFactory = ({
if (dto.type === "org") { if (dto.type === "org") {
ssoConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); ssoConfig = await samlConfigDAL.findOne({ orgId: dto.orgId });
if (!ssoConfig) return; if (!ssoConfig) return;
} else if (dto.type === "orgSlug") {
const org = await orgDAL.findOne({ slug: dto.orgSlug });
if (!org) return;
ssoConfig = await samlConfigDAL.findOne({ orgId: org.id });
} else if (dto.type === "ssoId") { } else if (dto.type === "ssoId") {
// TODO: // TODO:
// We made this change because saml config ids were not moved over during the migration // We made this change because saml config ids were not moved over during the migration
@ -227,7 +233,12 @@ export const samlConfigServiceFactory = ({
// when dto is type id means it's internally used // when dto is type id means it's internally used
if (dto.type === "org") { if (dto.type === "org") {
const { permission } = await permissionService.getOrgPermission(dto.actor, dto.actorId, ssoConfig.orgId); const { permission } = await permissionService.getOrgPermission(
dto.actor,
dto.actorId,
ssoConfig.orgId,
dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
} }
const { const {
@ -284,35 +295,20 @@ export const samlConfigServiceFactory = ({
isActive: ssoConfig.isActive, isActive: ssoConfig.isActive,
entryPoint, entryPoint,
issuer, issuer,
cert cert,
lastUsed: ssoConfig.lastUsed
}; };
}; };
const samlLogin = async ({ const samlLogin = async ({ firstName, email, lastName, authProvider, orgId, relayState }: TSamlLoginDTO) => {
firstName,
email,
lastName,
authProvider,
orgId,
relayState,
isSignupAllowed
}: TSamlLoginDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
let user = await userDAL.findUserByEmail(email); let user = await userDAL.findUserByEmail(email);
const isSamlSignUpDisabled = !isSignupAllowed && !user;
if (isSamlSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Saml SSO login" });
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new BadRequestError({ message: "Org not found" });
if (user) { if (user) {
const hasSamlEnabled = (user.authMethods || []).some((method) =>
Object.values(SamlProviders).includes(method as SamlProviders)
);
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
if (!hasSamlEnabled) {
await userDAL.updateById(user.id, { authMethods: [authProvider] }, tx);
}
const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx }); const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx });
if (!orgMembership) { if (!orgMembership) {
await orgDAL.createMembership( await orgDAL.createMembership(
@ -342,7 +338,8 @@ export const samlConfigServiceFactory = ({
email, email,
firstName, firstName,
lastName, lastName,
authMethods: [authProvider] authMethods: [AuthMethod.EMAIL],
isGhost: false
}, },
tx tx
); );
@ -378,6 +375,9 @@ export const samlConfigServiceFactory = ({
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
} }
); );
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
return { isUserCompleted, providerAuthToken }; return { isUserCompleted, providerAuthToken };
}; };

View File

@ -25,7 +25,11 @@ export type TUpdateSamlCfgDTO = Partial<{
TOrgPermission; TOrgPermission;
export type TGetSamlCfgDTO = export type TGetSamlCfgDTO =
| { type: "org"; orgId: string; actor: ActorType; actorId: string } | { type: "org"; orgId: string; actor: ActorType; actorId: string; actorOrgId?: string }
| {
type: "orgSlug";
orgSlug: string;
}
| { | {
type: "ssoId"; type: "ssoId";
id: string; id: string;
@ -37,7 +41,6 @@ export type TSamlLoginDTO = {
lastName?: string; lastName?: string;
authProvider: string; authProvider: string;
orgId: string; orgId: string;
isSignupAllowed: boolean;
// saml thingy // saml thingy
relayState?: string; relayState?: string;
}; };

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

@ -44,6 +44,7 @@ export const secretApprovalPolicyServiceFactory = ({
name, name,
actor, actor,
actorId, actorId,
actorOrgId,
approvals, approvals,
approvers, approvers,
projectId, projectId,
@ -53,7 +54,7 @@ export const secretApprovalPolicyServiceFactory = ({
if (approvals > approvers.length) if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
@ -96,13 +97,19 @@ export const secretApprovalPolicyServiceFactory = ({
name, name,
actorId, actorId,
actor, actor,
actorOrgId,
approvals, approvals,
secretPolicyId secretPolicyId
}: TUpdateSapDTO) => { }: TUpdateSapDTO) => {
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission(actor, actorId, secretApprovalPolicy.projectId); const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
secretApprovalPolicy.projectId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => { const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => {
@ -145,11 +152,16 @@ export const secretApprovalPolicyServiceFactory = ({
}; };
}; };
const deleteSecretApprovalPolicy = async ({ secretPolicyId, actor, actorId }: TDeleteSapDTO) => { const deleteSecretApprovalPolicy = async ({ secretPolicyId, actor, actorId, actorOrgId }: TDeleteSapDTO) => {
const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission(actor, actorId, sapPolicy.projectId); const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
sapPolicy.projectId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
@ -159,8 +171,8 @@ export const secretApprovalPolicyServiceFactory = ({
return sapPolicy; return sapPolicy;
}; };
const getSecretApprovalPolicyByProjectId = async ({ actorId, actor, projectId }: TListSapDTO) => { const getSecretApprovalPolicyByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TListSapDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId }); const sapPolicies = await secretApprovalPolicyDAL.find({ projectId });
@ -188,10 +200,11 @@ export const secretApprovalPolicyServiceFactory = ({
projectId, projectId,
actor, actor,
actorId, actorId,
actorOrgId,
environment, environment,
secretPath secretPath
}: TGetBoardSapDTO) => { }: TGetBoardSapDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment }) subject(ProjectPermissionSub.Secrets, { secretPath, environment })

View File

@ -1,8 +1,13 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretApprovalRequestsSecretsSchema, TableName, TSecretTags } from "@app/db/schemas"; import {
import { DatabaseError } from "@app/lib/errors"; SecretApprovalRequestsSecretsSchema,
TableName,
TSecretApprovalRequestsSecrets,
TSecretTags
} from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>; export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
@ -11,6 +16,35 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); 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) => { const findByRequestId = async (requestId: string, tx?: Knex) => {
try { try {
const doc = await (tx || db)({ const doc = await (tx || db)({
@ -190,6 +224,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
return { return {
...secretApprovalRequestSecretOrm, ...secretApprovalRequestSecretOrm,
findByRequestId, findByRequestId,
bulkUpdateNoVersionIncrement,
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany 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 { groupBy, pick, unique } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorType } from "@app/services/auth/auth-type"; 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 { TSecretQueueFactory } from "@app/services/secret/secret-queue";
import { TSecretServiceFactory } from "@app/services/secret/secret-service"; import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
@ -47,6 +48,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">; secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
secretService: Pick< secretService: Pick<
TSecretServiceFactory, TSecretServiceFactory,
| "fnSecretBulkInsert" | "fnSecretBulkInsert"
@ -67,16 +69,22 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequestReviewerDAL, secretApprovalRequestReviewerDAL,
secretApprovalRequestSecretDAL, secretApprovalRequestSecretDAL,
secretBlindIndexDAL, secretBlindIndexDAL,
projectDAL,
permissionService, permissionService,
snapshotService, snapshotService,
secretService, secretService,
secretVersionDAL, secretVersionDAL,
secretQueueService secretQueueService
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId }: TApprovalRequestCountDTO) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { membership } = await permissionService.getProjectPermission(actor as ActorType.USER, actorId, projectId); const { membership } = await permissionService.getProjectPermission(
actor as ActorType.USER,
actorId,
projectId,
actorOrgId
);
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id); const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id);
return count; return count;
@ -86,6 +94,7 @@ export const secretApprovalRequestServiceFactory = ({
projectId, projectId,
actorId, actorId,
actor, actor,
actorOrgId,
status, status,
environment, environment,
committer, committer,
@ -94,7 +103,7 @@ export const secretApprovalRequestServiceFactory = ({
}: TListApprovalsDTO) => { }: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId); const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
const approvals = await secretApprovalRequestDAL.findByProjectId({ const approvals = await secretApprovalRequestDAL.findByProjectId({
projectId, projectId,
committer, committer,
@ -107,7 +116,7 @@ export const secretApprovalRequestServiceFactory = ({
return approvals; return approvals;
}; };
const getSecretApprovalDetails = async ({ actor, actorId, id }: TSecretApprovalDetailsDTO) => { const getSecretApprovalDetails = async ({ actor, actorId, actorOrgId, id }: TSecretApprovalDetailsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id); const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
@ -117,7 +126,8 @@ export const secretApprovalRequestServiceFactory = ({
const { membership } = await permissionService.getProjectPermission( const { membership } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
secretApprovalRequest.projectId secretApprovalRequest.projectId,
actorOrgId
); );
if ( if (
membership.role !== ProjectMembershipRole.Admin && membership.role !== ProjectMembershipRole.Admin &&
@ -134,7 +144,7 @@ export const secretApprovalRequestServiceFactory = ({
return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets }; return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets };
}; };
const reviewApproval = async ({ approvalId, actor, status, actorId }: TReviewRequestDTO) => { const reviewApproval = async ({ approvalId, actor, status, actorId, actorOrgId }: TReviewRequestDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
@ -143,7 +153,8 @@ export const secretApprovalRequestServiceFactory = ({
const { membership } = await permissionService.getProjectPermission( const { membership } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
actorId, actorId,
secretApprovalRequest.projectId secretApprovalRequest.projectId,
actorOrgId
); );
if ( if (
membership.role !== ProjectMembershipRole.Admin && membership.role !== ProjectMembershipRole.Admin &&
@ -175,7 +186,7 @@ export const secretApprovalRequestServiceFactory = ({
return reviewStatus; return reviewStatus;
}; };
const updateApprovalStatus = async ({ actorId, status, approvalId, actor }: TStatusChangeDTO) => { const updateApprovalStatus = async ({ actorId, status, approvalId, actor, actorOrgId }: TStatusChangeDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
@ -184,7 +195,8 @@ export const secretApprovalRequestServiceFactory = ({
const { membership } = await permissionService.getProjectPermission( const { membership } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
actorId, actorId,
secretApprovalRequest.projectId secretApprovalRequest.projectId,
actorOrgId
); );
if ( if (
membership.role !== ProjectMembershipRole.Admin && membership.role !== ProjectMembershipRole.Admin &&
@ -207,13 +219,18 @@ export const secretApprovalRequestServiceFactory = ({
return { ...secretApprovalRequest, ...updatedRequest }; return { ...secretApprovalRequest, ...updatedRequest };
}; };
const mergeSecretApprovalRequest = async ({ approvalId, actor, actorId }: TMergeSecretApprovalRequestDTO) => { const mergeSecretApprovalRequest = async ({
approvalId,
actor,
actorId,
actorOrgId
}: TMergeSecretApprovalRequestDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy, folderId, projectId } = secretApprovalRequest; const { policy, folderId, projectId } = secretApprovalRequest;
const { membership } = await permissionService.getProjectPermission(ActorType.USER, actorId, projectId); const { membership } = await permissionService.getProjectPermission(ActorType.USER, actorId, projectId, actorOrgId);
if ( if (
membership.role !== ProjectMembershipRole.Admin && membership.role !== ProjectMembershipRole.Admin &&
secretApprovalRequest.committerId !== membership.id && secretApprovalRequest.committerId !== membership.id &&
@ -401,6 +418,7 @@ export const secretApprovalRequestServiceFactory = ({
data, data,
actorId, actorId,
actor, actor,
actorOrgId,
policy, policy,
projectId, projectId,
secretPath, secretPath,
@ -408,12 +426,19 @@ export const secretApprovalRequestServiceFactory = ({
}: TGenerateSecretApprovalRequestDTO) => { }: TGenerateSecretApprovalRequestDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission, membership } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
const folderId = folder.id; const folderId = folder.id;

View File

@ -14,13 +14,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/pr
import { TSecretRotationDALFactory } from "./secret-rotation-dal"; import { TSecretRotationDALFactory } from "./secret-rotation-dal";
import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
import { import { TCreateSecretRotationDTO, TDeleteDTO, TListByProjectIdDTO, TRestartDTO } from "./secret-rotation-types";
TCreateSecretRotationDTO,
TDeleteDTO,
TGetByIdDTO,
TListByProjectIdDTO,
TRestartDTO
} from "./secret-rotation-types";
import { rotationTemplates } from "./templates"; import { rotationTemplates } from "./templates";
type TSecretRotationServiceFactoryDep = { type TSecretRotationServiceFactoryDep = {
@ -45,8 +39,8 @@ export const secretRotationServiceFactory = ({
folderDAL, folderDAL,
secretDAL secretDAL
}: TSecretRotationServiceFactoryDep) => { }: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => { const getProviderTemplates = async ({ actor, actorId, actorOrgId, projectId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
return { return {
@ -59,6 +53,7 @@ export const secretRotationServiceFactory = ({
projectId, projectId,
actorId, actorId,
actor, actor,
actorOrgId,
inputs, inputs,
outputs, outputs,
interval, interval,
@ -66,7 +61,7 @@ export const secretRotationServiceFactory = ({
secretPath, secretPath,
environment environment
}: TCreateSecretRotationDTO) => { }: TCreateSecretRotationDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation ProjectPermissionSub.SecretRotation
@ -144,23 +139,14 @@ export const secretRotationServiceFactory = ({
return secretRotation; return secretRotation;
}; };
const getById = async ({ rotationId, actor, actorId }: TGetByIdDTO) => { const getByProjectId = async ({ actorId, projectId, actor, actorOrgId }: TListByProjectIdDTO) => {
const [doc] = await secretRotationDAL.find({ id: rotationId }); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
return doc;
};
const getByProjectId = async ({ actorId, projectId, actor }: TListByProjectIdDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
const doc = await secretRotationDAL.find({ projectId }); const doc = await secretRotationDAL.find({ projectId });
return doc; return doc;
}; };
const restartById = async ({ actor, actorId, rotationId }: TRestartDTO) => { const restartById = async ({ actor, actorId, actorOrgId, rotationId }: TRestartDTO) => {
const doc = await secretRotationDAL.findById(rotationId); const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" }); if (!doc) throw new BadRequestError({ message: "Rotation not found" });
@ -171,18 +157,18 @@ export const secretRotationServiceFactory = ({
message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation." message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
}); });
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
await secretRotationQueue.removeFromQueue(doc.id, doc.interval); await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
await secretRotationQueue.addToQueue(doc.id, doc.interval); await secretRotationQueue.addToQueue(doc.id, doc.interval);
return doc; return doc;
}; };
const deleteById = async ({ actor, actorId, rotationId }: TDeleteDTO) => { const deleteById = async ({ actor, actorId, actorOrgId, rotationId }: TDeleteDTO) => {
const doc = await secretRotationDAL.findById(rotationId); const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" }); if (!doc) throw new BadRequestError({ message: "Rotation not found" });
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation ProjectPermissionSub.SecretRotation
@ -197,7 +183,6 @@ export const secretRotationServiceFactory = ({
return { return {
getProviderTemplates, getProviderTemplates,
getById,
getByProjectId, getByProjectId,
createRotation, createRotation,
restartById, restartById,

View File

@ -18,7 +18,3 @@ export type TDeleteDTO = {
export type TRestartDTO = { export type TRestartDTO = {
rotationId: string; rotationId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetByIdDTO = {
rotationId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -39,8 +39,8 @@ export const secretScanningServiceFactory = ({
permissionService, permissionService,
secretScanningQueue secretScanningQueue
}: TSecretScanningServiceFactoryDep) => { }: TSecretScanningServiceFactoryDep) => {
const createInstallationSession = async ({ actor, orgId, actorId }: TInstallAppSessionDTO) => { const createInstallationSession = async ({ actor, orgId, actorId, actorOrgId }: TInstallAppSessionDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const sessionId = crypto.randomBytes(16).toString("hex"); const sessionId = crypto.randomBytes(16).toString("hex");
@ -48,11 +48,17 @@ export const secretScanningServiceFactory = ({
return { sessionId }; return { sessionId };
}; };
const linkInstallationToOrg = async ({ sessionId, actorId, installationId, actor }: TLinkInstallSessionDTO) => { const linkInstallationToOrg = async ({
sessionId,
actorId,
installationId,
actor,
actorOrgId
}: TLinkInstallSessionDTO) => {
const session = await gitAppInstallSessionDAL.findOne({ sessionId }); const session = await gitAppInstallSessionDAL.findOne({ sessionId });
if (!session) throw new UnauthorizedError({ message: "Session not found" }); if (!session) throw new UnauthorizedError({ message: "Session not found" });
const { permission } = await permissionService.getOrgPermission(actor, actorId, session.orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, session.orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const installatedApp = await gitAppOrgDAL.transaction(async (tx) => { const installatedApp = await gitAppOrgDAL.transaction(async (tx) => {
await gitAppInstallSessionDAL.deleteById(session.id, tx); await gitAppInstallSessionDAL.deleteById(session.id, tx);
@ -83,23 +89,23 @@ export const secretScanningServiceFactory = ({
return { installatedApp }; return { installatedApp };
}; };
const getOrgInstallationStatus = async ({ actorId, orgId, actor }: TGetOrgInstallStatusDTO) => { const getOrgInstallationStatus = async ({ actorId, orgId, actor, actorOrgId }: TGetOrgInstallStatusDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const appInstallation = await gitAppOrgDAL.findOne({ orgId }); const appInstallation = await gitAppOrgDAL.findOne({ orgId });
return Boolean(appInstallation); return Boolean(appInstallation);
}; };
const getRisksByOrg = async ({ actor, orgId, actorId }: TGetOrgRisksDTO) => { const getRisksByOrg = async ({ actor, orgId, actorId, actorOrgId }: TGetOrgRisksDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
return { risks }; return { risks };
}; };
const updateRiskStatus = async ({ actorId, orgId, actor, riskId, status }: TUpdateRiskStatusDTO) => { const updateRiskStatus = async ({ actorId, orgId, actor, actorOrgId, riskId, status }: TUpdateRiskStatusDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
const isRiskResolved = Boolean( const isRiskResolved = Boolean(

View File

@ -58,9 +58,10 @@ export const secretSnapshotServiceFactory = ({
projectId, projectId,
actorId, actorId,
actor, actor,
actorOrgId,
path path
}: TProjectSnapshotCountDTO) => { }: TProjectSnapshotCountDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
@ -75,11 +76,12 @@ export const secretSnapshotServiceFactory = ({
projectId, projectId,
actorId, actorId,
actor, actor,
actorOrgId,
path, path,
limit = 20, limit = 20,
offset = 0 offset = 0
}: TProjectSnapshotListDTO) => { }: TProjectSnapshotListDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
@ -89,10 +91,10 @@ export const secretSnapshotServiceFactory = ({
return snapshots; return snapshots;
}; };
const getSnapshotData = async ({ actorId, actor, id }: TGetSnapshotDataDTO) => { const getSnapshotData = async ({ actorId, actor, actorOrgId, id }: TGetSnapshotDataDTO) => {
const snapshot = await snapshotDAL.findSecretSnapshotDataById(id); const snapshot = await snapshotDAL.findSecretSnapshotDataById(id);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" }); if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
return snapshot; return snapshot;
}; };
@ -143,11 +145,11 @@ export const secretSnapshotServiceFactory = ({
} }
}; };
const rollbackSnapshot = async ({ id: snapshotId, actor, actorId }: TRollbackSnapshotDTO) => { const rollbackSnapshot = async ({ id: snapshotId, actor, actorId, actorOrgId }: TRollbackSnapshotDTO) => {
const snapshot = await snapshotDAL.findById(snapshotId); const snapshot = await snapshotDAL.findById(snapshotId);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" }); if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback

View File

@ -26,8 +26,8 @@ export const trustedIpServiceFactory = ({
licenseService, licenseService,
projectDAL projectDAL
}: TTrustedIpServiceFactoryDep) => { }: TTrustedIpServiceFactoryDep) => {
const listIpsByProjectId = async ({ projectId, actor, actorId }: TProjectPermission) => { const listIpsByProjectId = async ({ projectId, actor, actorId, actorOrgId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
const trustedIps = await trustedIpDAL.find({ const trustedIps = await trustedIpDAL.find({
projectId projectId
@ -35,8 +35,16 @@ export const trustedIpServiceFactory = ({
return trustedIps; return trustedIps;
}; };
const addProjectIp = async ({ projectId, actorId, actor, ipAddress: ip, comment, isActive }: TCreateIpDTO) => { const addProjectIp = async ({
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); projectId,
actorId,
actor,
actorOrgId,
ipAddress: ip,
comment,
isActive
}: TCreateIpDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
@ -65,8 +73,16 @@ export const trustedIpServiceFactory = ({
return { trustedIp, project }; // for audit log return { trustedIp, project }; // for audit log
}; };
const updateProjectIp = async ({ projectId, actorId, actor, ipAddress: ip, comment, trustedIpId }: TUpdateIpDTO) => { const updateProjectIp = async ({
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); projectId,
actorId,
actor,
actorOrgId,
ipAddress: ip,
comment,
trustedIpId
}: TUpdateIpDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
@ -97,8 +113,8 @@ export const trustedIpServiceFactory = ({
return { trustedIp, project }; // for audit log return { trustedIp, project }; // for audit log
}; };
const deleteProjectIp = async ({ projectId, actorId, actor, trustedIpId }: TDeleteIpDTO) => { const deleteProjectIp = async ({ projectId, actorId, actor, actorOrgId, trustedIpId }: TDeleteIpDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);

View File

@ -95,7 +95,7 @@ const envSchema = z
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()), SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()), SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
// LICENCE // LICENCE
LICENSE_SERVER_URL: zpStr(z.string().optional()), LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
LICENSE_SERVER_KEY: zpStr(z.string().optional()), LICENSE_SERVER_KEY: zpStr(z.string().optional()),
LICENSE_KEY: zpStr(z.string().optional()), LICENSE_KEY: zpStr(z.string().optional()),
STANDALONE_MODE: z STANDALONE_MODE: z

View File

@ -8,6 +8,9 @@ import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "../config/env"; import { getConfig } from "../config/env";
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
export type TDecryptSymmetricInput = { export type TDecryptSymmetricInput = {
ciphertext: string; ciphertext: string;
iv: string; iv: string;
@ -44,7 +47,7 @@ export const encryptSymmetric = (plaintext: string, key: string) => {
}; };
}; };
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) => { export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string | Buffer) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16); const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv); const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
@ -58,7 +61,12 @@ export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string)
}; };
}; };
export const decryptSymmetric128BitHexKeyUTF8 = ({ ciphertext, iv, tag, key }: TDecryptSymmetricInput): string => { export const decryptSymmetric128BitHexKeyUTF8 = ({
ciphertext,
iv,
tag,
key
}: Omit<TDecryptSymmetricInput, "key"> & { key: string | Buffer }): string => {
const decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64")); const decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64"));
decipher.setAuthTag(Buffer.from(tag, "base64")); decipher.setAuthTag(Buffer.from(tag, "base64"));

View File

@ -1,12 +1,20 @@
export { export {
buildSecretBlindIndexFromName, buildSecretBlindIndexFromName,
createSecretBlindIndex, createSecretBlindIndex,
decodeBase64,
decryptAsymmetric, decryptAsymmetric,
decryptSymmetric, decryptSymmetric,
decryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8,
encodeBase64,
encryptAsymmetric, encryptAsymmetric,
encryptSymmetric, encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8,
generateAsymmetricKeyPair generateAsymmetricKeyPair
} from "./encryption"; } from "./encryption";
export {
decryptIntegrationAuths,
decryptSecretApprovals,
decryptSecrets,
decryptSecretVersions
} from "./secret-encryption";
export { generateSrpServerKey, srpCheckClientProof } from "./srp"; 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 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) => { export const generateSrpServerKey = async (salt: string, verifier: string) => {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
@ -24,3 +32,99 @@ export const srpCheckClientProof = async (
server.setClientPublicKey(clientPublicKey); server.setClientPublicKey(clientPublicKey);
return server.checkClientProof(clientProof); 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; 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

@ -4,12 +4,14 @@ export type TOrgPermission = {
actor: ActorType; actor: ActorType;
actorId: string; actorId: string;
orgId: string; orgId: string;
actorOrgId?: string;
}; };
export type TProjectPermission = { export type TProjectPermission = {
actor: ActorType; actor: ActorType;
actorId: string; actorId: string;
projectId: string; projectId: string;
actorOrgId?: string;
}; };
export type RequiredKeys<T> = { export type RequiredKeys<T> = {

View File

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

View File

@ -37,7 +37,7 @@ type TMain = {
export const main = async ({ db, smtp, logger, queue }: TMain) => { export const main = async ({ db, smtp, logger, queue }: TMain) => {
const appCfg = getConfig(); const appCfg = getConfig();
const server = fasitfy({ const server = fasitfy({
logger, logger: appCfg.NODE_ENV === "test" ? false : logger,
trustProxy: true, trustProxy: true,
connectionTimeout: 30 * 1000, connectionTimeout: 30 * 1000,
ignoreTrailingSlash: true ignoreTrailingSlash: true

View File

@ -0,0 +1,17 @@
import { FastifyRequest } from "fastify";
import { ActorType } from "@app/services/auth/auth-type";
// this is a unique id for sending posthog event
export const getTelemetryDistinctId = (req: FastifyRequest) => {
if (req.auth.actor === ActorType.USER) {
return req.auth.user.email;
}
if (req.auth.actor === ActorType.IDENTITY) {
return `identity-${req.auth.identityId}`;
}
if (req.auth.actor === ActorType.SERVICE) {
return req.auth.serviceToken.createdByEmail || `service-token-null-creator-${req.auth.serviceTokenId}`; // when user gets removed from system
}
return "unknown-auth-data";
};

View File

@ -63,6 +63,11 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
identityId: req.auth.identityId identityId: req.auth.identityId
} }
}; };
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
payload.actor = {
type: ActorType.SCIM_CLIENT,
metadata: {}
};
} else { } else {
throw new BadRequestError({ message: "Missing logic for other actor" }); 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 jwt, { JwtPayload } from "jsonwebtoken";
import { TServiceTokens, TUsers } from "@app/db/schemas"; import { TServiceTokens, TUsers } from "@app/db/schemas";
import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
@ -10,6 +11,7 @@ import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-to
export type TAuthMode = export type TAuthMode =
| { | {
orgId?: string;
authMode: AuthMode.JWT; authMode: AuthMode.JWT;
actor: ActorType.USER; actor: ActorType.USER;
userId: string; userId: string;
@ -21,10 +23,11 @@ export type TAuthMode =
actor: ActorType.USER; actor: ActorType.USER;
userId: string; userId: string;
user: TUsers; user: TUsers;
orgId?: string;
} }
| { | {
authMode: AuthMode.SERVICE_TOKEN; authMode: AuthMode.SERVICE_TOKEN;
serviceToken: TServiceTokens; serviceToken: TServiceTokens & { createdByEmail: string };
actor: ActorType.SERVICE; actor: ActorType.SERVICE;
serviceTokenId: string; serviceTokenId: string;
} }
@ -33,6 +36,12 @@ export type TAuthMode =
actor: ActorType.IDENTITY; actor: ActorType.IDENTITY;
identityId: string; identityId: string;
identityName: string; identityName: string;
}
| {
authMode: AuthMode.SCIM_TOKEN;
actor: ActorType.SCIM_CLIENT;
scimTokenId: string;
orgId: string;
}; };
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
@ -53,6 +62,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
} }
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload; const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
switch (decodedToken.authTokenType) { switch (decodedToken.authTokenType) {
case AuthTokenType.ACCESS_TOKEN: case AuthTokenType.ACCESS_TOKEN:
return { return {
@ -68,6 +78,12 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
token: decodedToken as TIdentityAccessTokenJwtPayload, token: decodedToken as TIdentityAccessTokenJwtPayload,
actor: ActorType.IDENTITY actor: ActorType.IDENTITY
} as const; } as const;
case AuthTokenType.SCIM_TOKEN:
return {
authMode: AuthMode.SCIM_TOKEN,
token: decodedToken as TScimTokenJwtPayload,
actor: ActorType.SCIM_CLIENT
} as const;
default: default:
return { authMode: null, token: null } as const; return { authMode: null, token: null } as const;
} }
@ -82,8 +98,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
switch (authMode) { switch (authMode) {
case AuthMode.JWT: { case AuthMode.JWT: {
const { user, tokenVersionId } = await server.services.authToken.fnValidateJwtIdentity(token); const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token);
req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor }; req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, orgId };
break; break;
} }
case AuthMode.IDENTITY_ACCESS_TOKEN: { case AuthMode.IDENTITY_ACCESS_TOKEN: {
@ -111,6 +127,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user }; req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
break; 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: default:
throw new UnauthorizedError({ name: "Unknown token strategy" }); throw new UnauthorizedError({ name: "Unknown token strategy" });
} }

View File

@ -9,11 +9,13 @@ export const injectPermission = fp(async (server) => {
if (!req.auth) return; if (!req.auth) return;
if (req.auth.actor === ActorType.USER) { if (req.auth.actor === ActorType.USER) {
req.permission = { type: ActorType.USER, id: req.auth.userId }; req.permission = { type: ActorType.USER, id: req.auth.userId, orgId: req.auth?.orgId };
} else if (req.auth.actor === ActorType.IDENTITY) { } else if (req.auth.actor === ActorType.IDENTITY) {
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId }; req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
} else if (req.auth.actor === ActorType.SERVICE) { } else if (req.auth.actor === ActorType.SERVICE) {
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId }; 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 fastifyPlugin from "fastify-plugin";
import { ZodError } from "zod"; 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) => { export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
server.setErrorHandler((error, req, res) => { server.setErrorHandler((error, req, res) => {
@ -21,6 +27,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
error: "PermissionDenied", error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType}` 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 { } else {
void res.send(error); void res.send(error);
} }

View File

@ -25,13 +25,13 @@ export const fastifySwagger = fp(async (fastify) => {
], ],
components: { components: {
securitySchemes: { securitySchemes: {
bearer: { bearerAuth: {
type: "http", type: "http",
scheme: "bearer", scheme: "bearer",
bearerFormat: "JWT", bearerFormat: "JWT",
description: "A service token in Infisical" description: "An access token in Infisical"
}, },
apiKey: { apiKeyAuth: {
type: "apiKey", type: "apiKey",
in: "header", in: "header",
name: "X-API-Key", name: "X-API-Key",

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